From 39aa96e6c4e455d9fc0fd8480bedabd06c79d7bd Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 13 Mar 2026 10:58:10 -0700 Subject: [PATCH 01/36] commit for review --- src/AppInstallerCLITests/SQLiteIndex.cpp | 253 ++++++++ .../AppInstallerRepositoryCore.vcxproj | 2 + .../Microsoft/SQLiteIndex.cpp | 33 ++ .../Microsoft/SQLiteIndex.h | 7 + .../Microsoft/Schema/2_0/Interface.h | 8 + .../Microsoft/Schema/2_0/Interface_2_0.cpp | 528 ++++++++++++++++- .../Schema/2_0/PackageUpdateTrackingTable.cpp | 109 +++- .../Schema/2_0/PackageUpdateTrackingTable.h | 6 + .../Microsoft/Schema/2_0/PackagesTable.cpp | 26 + .../Microsoft/Schema/2_0/PackagesTable.h | 3 + .../Microsoft/Schema/2_1/Interface.h | 21 + .../Microsoft/Schema/2_1/Interface_2_1.cpp | 34 ++ .../Microsoft/Schema/ISQLiteIndex.cpp | 7 +- .../Microsoft/Schema/SQLiteIndexContextData.h | 16 + src/WinGetUtil/Exports.cpp | 18 + src/WinGetUtil/WinGetUtil.h | 8 + src/WinGetUtilInterop/Api/WinGetFactory.cs | 24 + .../Interfaces/IWinGetFactory.cs | 8 + .../Interfaces/IWinGetSQLiteIndex.cs | 12 + .../DeltaIndexTestTool.csproj | 29 + tools/DeltaIndexTestTool/Program.cs | 541 ++++++++++++++++++ 21 files changed, 1675 insertions(+), 18 deletions(-) create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp create mode 100644 tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj create mode 100644 tools/DeltaIndexTestTool/Program.cs diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index e15e72e50c..aaa071f8a9 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -25,6 +25,9 @@ #include #include +#include +#include + using namespace std::string_literals; using namespace std::string_view_literals; using namespace TestCommon; @@ -3963,3 +3966,253 @@ TEST_CASE("SQLiteIndex_VersionStringPreserved", "[sqliteindex]") REQUIRE(extractedVersion == version); } + +TEST_CASE("SQLiteIndex_Delta_AddedPackage", "[sqliteindex][V2_1][delta]") +{ + TempFile workingFile{ "delta_working"s, ".db"s }; + TempFile baselineFile{ "delta_baseline"s, ".db"s }; + TempFile deltaFile{ "delta_output"s, ".db"s }; + + // Build baseline: one package "Publisher1" + { + SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + index.AddManifest(m1.Manifest, m1.Path); + + std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + } + + // Add a new package to the working index and generate a delta + { + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); + + // Sleep 1s to ensure Publisher2's write time is after the new base time + std::this_thread::sleep_for(std::chrono::seconds(1)); + + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + index.AddManifest(m2.Manifest, m2.Path); + + index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); + index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); + index.PrepareForPackaging(); + } + + // The delta file should exist and contain the new package + REQUIRE(std::filesystem::exists(deltaFile.GetPath())); + + Connection deltaConn = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + + Statement countStmt = Statement::Create(deltaConn, "SELECT COUNT(*) FROM delta_packages WHERE is_removed = 0"); + REQUIRE(countStmt.Step()); + REQUIRE(countStmt.GetColumn(0) == 1); + + Statement idStmt = Statement::Create(deltaConn, "SELECT id FROM delta_packages WHERE is_removed = 0"); + REQUIRE(idStmt.Step()); + REQUIRE(idStmt.GetColumn(0) == "Publisher2.Id"); +} + +TEST_CASE("SQLiteIndex_Delta_RemovedPackage", "[sqliteindex][V2_1][delta]") +{ + TempFile workingFile{ "delta_working"s, ".db"s }; + TempFile baselineFile{ "delta_baseline"s, ".db"s }; + TempFile deltaFile{ "delta_output"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + // Build baseline: two packages + { + SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + + std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + } + + // Remove Publisher2 from the working index and generate a delta + { + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); + + // Sleep 1s to ensure Publisher2 removal write time is after the new base time + std::this_thread::sleep_for(std::chrono::seconds(1)); + + index.RemoveManifest(m2.Manifest, m2.Path); + + index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); + index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); + index.PrepareForPackaging(); + } + + REQUIRE(std::filesystem::exists(deltaFile.GetPath())); + + Connection deltaConn = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + + Statement countStmt = Statement::Create(deltaConn, "SELECT COUNT(*) FROM delta_packages WHERE is_removed = 1"); + REQUIRE(countStmt.Step()); + REQUIRE(countStmt.GetColumn(0) == 1); + + Statement idStmt = Statement::Create(deltaConn, "SELECT id FROM delta_packages WHERE is_removed = 1"); + REQUIRE(idStmt.Step()); + REQUIRE(idStmt.GetColumn(0) == "Publisher2.Id"); +} + +TEST_CASE("SQLiteIndex_Delta_NoChanges_NoDeltaFile", "[sqliteindex][V2_1][delta]") +{ + TempFile workingFile{ "delta_working"s, ".db"s }; + TempFile baselineFile{ "delta_baseline"s, ".db"s }; + TempFile deltaFile{ "delta_output"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + + // Build baseline + { + SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + + std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + } + + // Set tracking base to "now" so no packages appear changed + { + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); // records current time + + index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); + index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); + index.PrepareForPackaging(); + } + + // No changes tracked after setting base time, so delta file should NOT have been created + REQUIRE(!std::filesystem::exists(deltaFile.GetPath())); +} + +TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_Search", "[sqliteindex][V2_1][delta]") +{ + TempFile workingFile{ "delta_working"s, ".db"s }; + TempFile baselineFile{ "delta_baseline"s, ".db"s }; + TempFile workingFile2{ "delta_working2"s, ".db"s }; + TempFile deltaFile{ "delta_output"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + + // Build baseline with Publisher1 + { + SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + + std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + } + + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + // Add Publisher2 to working copy and generate delta + { + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + index.AddManifest(m2.Manifest, m2.Path); + + index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); + index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); + index.PrepareForPackaging(); + } + + REQUIRE(std::filesystem::exists(deltaFile.GetPath())); + + // Open the delta combined with the baseline + SQLiteIndex combined = SQLiteIndex::OpenWithBaseline( + deltaFile.GetPath().u8string(), + baselineFile.GetPath().u8string()); + + // Search should return both Publisher1 (from baseline) and Publisher2 (from delta) + auto results = combined.Search({}); + REQUIRE(results.Matches.size() == 2); + + std::set foundIds; + for (const auto& match : results.Matches) + { + auto id = combined.GetPropertyByPrimaryId(match.first, PackageVersionProperty::Id); + REQUIRE(id.has_value()); + foundIds.insert(id.value()); + } + + REQUIRE(foundIds.count("Publisher1.Id") == 1); + REQUIRE(foundIds.count("Publisher2.Id") == 1); +} + +TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_RemovedPackageExcluded", "[sqliteindex][V2_1][delta]") +{ + TempFile workingFile{ "delta_working"s, ".db"s }; + TempFile baselineFile{ "delta_baseline"s, ".db"s }; + TempFile deltaFile{ "delta_output"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + // Build baseline with two packages + { + SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + + std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + } + + // Remove Publisher2 and generate delta + { + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + index.RemoveManifest(m2.Manifest, m2.Path); + + index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); + index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); + index.PrepareForPackaging(); + } + + REQUIRE(std::filesystem::exists(deltaFile.GetPath())); + + // Open combined: should show only Publisher1 (Publisher2 removed) + SQLiteIndex combined = SQLiteIndex::OpenWithBaseline( + deltaFile.GetPath().u8string(), + baselineFile.GetPath().u8string()); + + auto results = combined.Search({}); + REQUIRE(results.Matches.size() == 1); + + auto id = combined.GetPropertyByPrimaryId(results.Matches[0].first, PackageVersionProperty::Id); + REQUIRE(id.has_value()); + REQUIRE(id.value() == "Publisher1.Id"); +} + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index 53a8260f9c..39ef5e2749 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -331,6 +331,7 @@ + @@ -441,6 +442,7 @@ + diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp index ba4c089158..c75ec42487 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -5,6 +5,7 @@ #include #include "ArpVersionValidation.h" #include +#include "Microsoft/Schema/2_0/Interface.h" namespace AppInstaller::Repository::Microsoft { @@ -37,6 +38,24 @@ namespace AppInstaller::Repository::Microsoft return { filePath, source }; } + SQLiteIndex SQLiteIndex::OpenWithBaseline(const std::string& deltaFilePath, const std::string& baselineFilePath) + { + AICLI_LOG(Repo, Info, << "Opening delta index [" << deltaFilePath << "] with baseline [" << baselineFilePath << "]"); + SQLiteIndex result{ deltaFilePath, SQLiteStorageBase::OpenDisposition::ReadOnly }; + + std::filesystem::path baselinePath{ Utility::ConvertToUTF16(baselineFilePath) }; + THROW_HR_IF(E_INVALIDARG, baselinePath.empty() || baselinePath.is_relative()); + + result.m_contextData.Add(baselinePath); + + // The interface must be V2_0 to support delta read mode + auto* v2Interface = dynamic_cast(result.m_interface.get()); + THROW_HR_IF(E_NOTIMPL, v2Interface == nullptr); + v2Interface->SetupDeltaReadMode(result.m_dbconn, baselinePath); + + return result; + } + SQLiteIndex::SQLiteIndex(const std::string& target, const SQLite::Version& version) : SQLiteStorageBase(target, version) { m_dbconn.EnableICU(); @@ -366,6 +385,20 @@ namespace AppInstaller::Repository::Microsoft m_contextData.Add(std::move(pathValue)); } break; + case Property::DeltaBaselineIndexPath: + { + std::filesystem::path pathValue{ Utility::ConvertToUTF16(value) }; + THROW_HR_IF(E_INVALIDARG, pathValue.empty() || pathValue.is_relative()); + m_contextData.Add(std::move(pathValue)); + } + break; + case Property::DeltaOutputPath: + { + std::filesystem::path pathValue{ Utility::ConvertToUTF16(value) }; + THROW_HR_IF(E_INVALIDARG, pathValue.empty() || pathValue.is_relative()); + m_contextData.Add(std::move(pathValue)); + } + break; } } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h index 6dc3cac54e..592b4c9589 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h @@ -55,6 +55,11 @@ namespace AppInstaller::Repository::Microsoft // Creates a copy of the given index. static SQLiteIndex CopyFrom(const std::string& filePath, SQLiteIndex& source); + // Opens a delta index combined with its baseline for reading. + // The delta is the main connection; the baseline is ATTACHed and TEMP VIEWs are created + // so that existing search code operates transparently across both. + static SQLiteIndex OpenWithBaseline(const std::string& deltaFilePath, const std::string& baselineFilePath); + #ifndef AICLI_DISABLE_TEST_HOOKS // Changes the version of the interface being used to operate on the database. // Should only be used for testing. @@ -167,6 +172,8 @@ namespace AppInstaller::Repository::Microsoft { PackageUpdateTrackingBaseTime, IntermediateFileOutputPath, + DeltaBaselineIndexPath, + DeltaOutputPath, }; // Sets the given property. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h index 2b86d1e32e..cf2d038eed 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h @@ -55,6 +55,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 bool MigrateFrom(SQLite::Connection& connection, const ISQLiteIndex* current) override; void SetProperty(SQLite::Connection& connection, Property property, const std::string& value) override; + // Sets up this index to act as a composed (delta + baseline) read-only view. + // Attaches the baseline database and creates TEMP VIEWs that union delta + baseline data. + // Must be called before any read operations on a delta index. + void SetupDeltaReadMode(SQLite::Connection& connection, const std::filesystem::path& baselinePath); + protected: // Creates the search results table. virtual std::unique_ptr CreateSearchResultsTable(const SQLite::Connection& connection) const; @@ -89,6 +94,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // If EnsureInternalInterface has been called. mutable bool m_internalInterfaceChecked = false; + // Set to true after SetupDeltaReadMode; prevents EnsureInternalInterface from creating the V1.7 interface. + mutable bool m_isDeltaReadMode = false; + // Interface to the data before PrepareForPackaging is called. mutable std::unique_ptr m_internalInterface; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index 79c1f03bea..f449616465 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include +#include #include "Microsoft/Schema/2_0/Interface.h" #include "Microsoft/Schema/2_0/PackagesTable.h" @@ -16,6 +17,7 @@ #include "Microsoft/Schema/2_0/SearchResultsTable.h" #include "Microsoft/Schema/2_0/PackageUpdateTrackingTable.h" +#include "Microsoft/Schema/1_0/IdTable.h" #include @@ -646,6 +648,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Output all of the changed package version manifests since the base time to the target location for (const auto& packageData : PackageUpdateTrackingTable::GetUpdatesSince(connection, updateBaseTime)) { + if (packageData.IsRemoved) + { + continue; + } + std::filesystem::path packageDirectory = baseOutputDirectory / Manifest::PackageVersionDataManifest::GetRelativeDirectoryPath(packageData.PackageIdentifier, Utility::SHA256::ConvertToString(packageData.Hash)); @@ -712,7 +719,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 addIfPresent(PackagesTable::ARPMinVersionColumn::Name, m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::ArpMinVersion).value()); addIfPresent(PackagesTable::ARPMaxVersionColumn::Name, m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::ArpMaxVersion).value()); - SQLite::rowid_t packageId = PackagesTable::Insert(connection, packageData); + auto idRowId = V1_0::IdTable::SelectIdByValue(connection, packageIdentifier); + THROW_HR_IF(E_NOT_VALID_STATE, !idRowId); + + SQLite::rowid_t packageId = PackagesTable::InsertWithRowId(connection, idRowId.value(), packageData); PackagesTable::UpdateValueIdById(connection, packageId, PackageUpdateTrackingTable::GetDataHash(connection, packageIdentifier)); @@ -742,6 +752,140 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 TagsTable::PrepareForPackaging(connection); CommandsTable::PrepareForPackaging(connection); + // Generate the delta index before dropping the tracking table (which is needed for delta construction). + // Delta generation is triggered by setting DeltaBaselineIndexPath and DeltaOutputPath on the context. + if (context.Data.Contains(Property::DeltaBaselineIndexPath) && + context.Data.Contains(Property::DeltaOutputPath)) + { + // Delta packaging requires schema 2.1+ (is_removed column in update_tracking). + THROW_WIN32_IF(ERROR_NOT_SUPPORTED, GetVersion().MinorVersion < 1); + + std::filesystem::path baselinePath = context.Data.Get(); + std::filesystem::path deltaOutputPath = context.Data.Get(); + + AICLI_LOG(Repo, Info, << "Generating delta index at [" << deltaOutputPath << "] against baseline [" << baselinePath << "]"); + + int64_t deltaUpdateBaseTime = 0; + std::optional deltaUpdateBaseTimeString = SQLite::MetadataTable::TryGetNamedValue(connection, s_MetadataValueName_PackageUpdateTrackingBaseTime); + if (deltaUpdateBaseTimeString && !deltaUpdateBaseTimeString->empty()) + { + deltaUpdateBaseTime = std::stoll(deltaUpdateBaseTimeString.value()); + } + + auto changedPackages = PackageUpdateTrackingTable::GetUpdatesSince(connection, deltaUpdateBaseTime); + if (changedPackages.empty()) + { + AICLI_LOG(Repo, Info, << "No changed packages found; skipping delta generation"); + } + else + { + SQLite::Connection baselineConn = SQLite::Connection::Create( + baselinePath.u8string(), SQLite::Connection::OpenDisposition::ReadOnly); + + SQLite::Connection deltaConn = SQLite::Connection::Create( + deltaOutputPath.u8string(), SQLite::Connection::OpenDisposition::Create); + + anon::CreateDeltaSchema(deltaConn); + + SQLite::rowid_t maxBaselinePackageRowid = anon::GetMaxPackageRowid(baselineConn); + SQLite::rowid_t nextNewPackageRowid = maxBaselinePackageRowid; + + SQLite::rowid_t maxBaselineTagsRowid = anon::GetMaxDataTableRowid(baselineConn, "tags2"); + SQLite::rowid_t nextNewTagsRowid = maxBaselineTagsRowid; + + SQLite::rowid_t maxBaselineCommandsRowid = anon::GetMaxDataTableRowid(baselineConn, "commands2"); + SQLite::rowid_t nextNewCommandsRowid = maxBaselineCommandsRowid; + + SQLite::Savepoint deltaSavepoint = SQLite::Savepoint::Create(deltaConn, "delta_build"); + + for (const auto& pkg : changedPackages) + { + SQLite::rowid_t packageRowid = anon::GetBaselinePackageRowid(baselineConn, pkg.PackageIdentifier); + + if (pkg.IsRemoved) + { + if (packageRowid == 0) + { + // Package was added and removed within the same tracking window; skip. + continue; + } + + AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); + + std::string sql = "INSERT OR REPLACE INTO delta_packages (rowid, id, name, latest_version, is_removed) VALUES (?, ?, '', '', 1)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, packageRowid); + stmt.Bind(2, pkg.PackageIdentifier); + stmt.Execute(); + } + else + { + bool isNewPackage = (packageRowid == 0); + if (isNewPackage) + { + packageRowid = ++nextNewPackageRowid; + } + + AICLI_LOG(Repo, Verbose, << "Delta: recording " << (isNewPackage ? "addition" : "update") << " of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); + + { + std::string sql = "SELECT id, name, moniker, latest_version, arp_min_version, arp_max_version, hash " + "FROM packages WHERE id = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(connection, sql); + stmt.Bind(1, pkg.PackageIdentifier); + THROW_HR_IF(E_NOT_SET, !stmt.Step()); + + std::string id = stmt.GetColumn(0); + std::string name = stmt.GetColumn(1); + std::string moniker = stmt.GetColumnIsNull(2) ? "" : stmt.GetColumn(2); + std::string latestVersion = stmt.GetColumn(3); + std::string arpMin = stmt.GetColumnIsNull(4) ? "" : stmt.GetColumn(4); + std::string arpMax = stmt.GetColumnIsNull(5) ? "" : stmt.GetColumn(5); + SQLite::blob_t hash = stmt.GetColumnIsNull(6) ? SQLite::blob_t{} : stmt.GetColumn(6); + + std::string insertSql = + "INSERT OR REPLACE INTO delta_packages " + "(rowid, id, name, moniker, latest_version, arp_min_version, arp_max_version, hash, is_removed) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)"; + SQLite::Statement insertStmt = SQLite::Statement::Create(deltaConn, insertSql); + insertStmt.Bind(1, packageRowid); + insertStmt.Bind(2, id); + insertStmt.Bind(3, name); + if (moniker.empty()) insertStmt.Bind(4, nullptr); else insertStmt.Bind(4, moniker); + insertStmt.Bind(5, latestVersion); + if (arpMin.empty()) insertStmt.Bind(6, nullptr); else insertStmt.Bind(6, arpMin); + if (arpMax.empty()) insertStmt.Bind(7, nullptr); else insertStmt.Bind(7, arpMax); + if (hash.empty()) insertStmt.Bind(8, nullptr); else insertStmt.Bind(8, hash); + insertStmt.Execute(); + } + + static constexpr std::pair s_DeltaSysRefTables[] = { + { "pfns2", "pfn" }, + { "productcodes2", "productcode" }, + { "norm_names2", "norm_name" }, + { "norm_publishers2", "norm_publisher" }, + { "upgradecodes2", "upgradecode" }, + }; + + for (const auto& [table, value] : s_DeltaSysRefTables) + { + anon::ProcessDeltaSysRefTable(deltaConn, connection, baselineConn, + table, value, packageRowid, pkg.PackageIdentifier); + } + + anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, + "tags2", "tag", packageRowid, nextNewTagsRowid); + anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, + "commands2", "command", packageRowid, nextNewCommandsRowid); + } + } + + deltaSavepoint.Commit(); + + AICLI_LOG(Repo, Info, << "Delta index generation complete"); + } + } + PackageUpdateTrackingTable::Drop(connection); // The tables based on SystemReferenceStringTable don't need a prepare currently @@ -766,11 +910,314 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 builder.Execute(connection); } + namespace anon + { + // Executes a raw SQL statement on a connection using the statement builder mechanism. + void ExecuteSQL(SQLite::Connection& connection, std::string_view sql) + { + SQLite::Statement stmt = SQLite::Statement::Create(connection, sql); + stmt.Execute(); + } + + // Creates all delta tables in the delta connection. + void CreateDeltaSchema(SQLite::Connection& deltaConn) + { + ExecuteSQL(deltaConn, R"( + CREATE TABLE IF NOT EXISTS delta_packages ( + rowid INTEGER PRIMARY KEY, + id TEXT NOT NULL, + name TEXT NOT NULL, + moniker TEXT, + latest_version TEXT NOT NULL, + arp_min_version TEXT, + arp_max_version TEXT, + hash BLOB, + is_removed INTEGER NOT NULL DEFAULT 0 + ) + )"); + + // SystemReference string tables (value + package_id, no separate id) + static constexpr std::pair s_SysRefTables[] = { + { "pfns2", "pfn" }, + { "productcodes2", "productcode" }, + { "norm_names2", "norm_name" }, + { "norm_publishers2", "norm_publisher" }, + { "upgradecodes2", "upgradecode" }, + }; + for (const auto& [table, value] : s_SysRefTables) + { + std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + + " (" + std::string(value) + " TEXT NOT NULL, package INTEGER NOT NULL, " + + "is_removed INTEGER NOT NULL DEFAULT 0, " + + "PRIMARY KEY (" + std::string(value) + ", package)) WITHOUT ROWID"; + ExecuteSQL(deltaConn, sql); + } + + // OneToMany data tables (rowid + value) + static constexpr std::pair s_OneToManyTables[] = { + { "tags2", "tag" }, + { "commands2", "command" }, + }; + for (const auto& [table, value] : s_OneToManyTables) + { + std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + + " (rowid INTEGER PRIMARY KEY, " + std::string(value) + " TEXT NOT NULL)"; + ExecuteSQL(deltaConn, sql); + } + + // OneToMany map tables (value_rowid + package_rowid) + for (const auto& [table, value] : s_OneToManyTables) + { + std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + "_map" + + " (" + std::string(value) + " INTEGER NOT NULL, package INTEGER NOT NULL, " + + "is_removed INTEGER NOT NULL DEFAULT 0, " + + "PRIMARY KEY (" + std::string(value) + ", package)) WITHOUT ROWID"; + ExecuteSQL(deltaConn, sql); + } + } + + // Returns the rowid of a package in the baseline, or 0 if not found. + SQLite::rowid_t GetBaselinePackageRowid(SQLite::Connection& baselineConn, const std::string& packageId) + { + SQLite::Builder::StatementBuilder builder; + builder.Select(SQLite::RowIDName).From("packages").Where("id").Equals(packageId); + SQLite::Statement stmt = builder.Prepare(baselineConn); + if (stmt.Step()) + { + return stmt.GetColumn(0); + } + return 0; + } + + // Returns the max rowid in the packages table, or 0 if empty. + SQLite::rowid_t GetMaxPackageRowid(SQLite::Connection& baselineConn) + { + SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, "SELECT MAX(rowid) FROM packages"); + if (stmt.Step()) + { + // MAX(rowid) returns NULL if table is empty + if (!stmt.GetColumnIsNull(0)) + { + return stmt.GetColumn(0); + } + } + return 0; + } + + // Returns the max rowid in a data table (tags2 or commands2), or 0 if empty. + SQLite::rowid_t GetMaxDataTableRowid(SQLite::Connection& baselineConn, std::string_view tableName) + { + std::string sql = "SELECT MAX(rowid) FROM " + std::string(tableName); + SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); + if (stmt.Step() && !stmt.GetColumnIsNull(0)) + { + return stmt.GetColumn(0); + } + return 0; + } + + // Returns the rowid in baseline data table for the given value string, or 0 if not present. + SQLite::rowid_t GetBaselineDataTableRowid(SQLite::Connection& baselineConn, std::string_view tableName, std::string_view valueName, const std::string& value) + { + std::string sql = "SELECT rowid FROM " + std::string(tableName) + " WHERE " + std::string(valueName) + " = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); + stmt.Bind(1, value); + if (stmt.Step()) + { + return stmt.GetColumn(0); + } + return 0; + } + + // Inserts or finds a value in delta data table; returns the rowid (possibly from baseline). + // baselineMaxRowid: the starting offset for new delta rowids. + SQLite::rowid_t EnsureDeltaDataTableValue( + SQLite::Connection& deltaConn, + SQLite::Connection& baselineConn, + std::string_view deltaTableName, + std::string_view valueName, + const std::string& value, + SQLite::rowid_t& nextNewRowid) + { + // Check if the value is already in the baseline + SQLite::rowid_t baselineRowid = GetBaselineDataTableRowid(baselineConn, std::string(deltaTableName).substr(6), valueName, value); + if (baselineRowid != 0) + { + return baselineRowid; + } + + // Check if already in the delta table + std::string selectSql = "SELECT rowid FROM " + std::string(deltaTableName) + " WHERE " + std::string(valueName) + " = ?"; + SQLite::Statement selectStmt = SQLite::Statement::Create(deltaConn, selectSql); + selectStmt.Bind(1, value); + if (selectStmt.Step()) + { + return selectStmt.GetColumn(0); + } + + // Insert as a new entry + SQLite::rowid_t newRowid = ++nextNewRowid; + std::string insertSql = "INSERT INTO " + std::string(deltaTableName) + " (rowid, " + std::string(valueName) + ") VALUES (?, ?)"; + SQLite::Statement insertStmt = SQLite::Statement::Create(deltaConn, insertSql); + insertStmt.Bind(1, newRowid); + insertStmt.Bind(2, value); + insertStmt.Execute(); + return newRowid; + } + + // Processes a SystemReference table for a changed package. + // Compares current values vs baseline values and records adds/removes. + void ProcessDeltaSysRefTable( + SQLite::Connection& deltaConn, + SQLite::Connection& sourceConn, + SQLite::Connection& baselineConn, + std::string_view tableName, + std::string_view valueName, + SQLite::rowid_t packageRowid, + const std::string& packageId) + { + std::string deltaTable = "delta_" + std::string(tableName); + std::string primaryCol = "package"; + + // Get current values from the new V2 index + std::vector currentValues; + { + std::string sql = "SELECT " + std::string(valueName) + " FROM " + std::string(tableName) + " WHERE " + primaryCol + " = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(sourceConn, sql); + stmt.Bind(1, packageRowid); + while (stmt.Step()) + { + currentValues.push_back(stmt.GetColumn(0)); + } + } + + // Get baseline values + std::vector baselineValues; + { + std::string sql = "SELECT " + std::string(valueName) + " FROM " + std::string(tableName) + " WHERE " + primaryCol + " = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); + stmt.Bind(1, packageRowid); + while (stmt.Step()) + { + baselineValues.push_back(stmt.GetColumn(0)); + } + } + + // Find added values (in current but not baseline) + for (const auto& val : currentValues) + { + if (std::find(baselineValues.begin(), baselineValues.end(), val) == baselineValues.end()) + { + std::string sql = "INSERT OR IGNORE INTO " + deltaTable + + " (" + std::string(valueName) + ", " + primaryCol + ", is_removed) VALUES (?, ?, 0)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, val); + stmt.Bind(2, packageRowid); + stmt.Execute(); + } + } + + // Find removed values (in baseline but not current) + for (const auto& val : baselineValues) + { + if (std::find(currentValues.begin(), currentValues.end(), val) == currentValues.end()) + { + std::string sql = "INSERT OR IGNORE INTO " + deltaTable + + " (" + std::string(valueName) + ", " + primaryCol + ", is_removed) VALUES (?, ?, 1)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, val); + stmt.Bind(2, packageRowid); + stmt.Execute(); + } + } + } + + // Processes a OneToMany table for a changed package. + void ProcessDeltaOneToManyTable( + SQLite::Connection& deltaConn, + SQLite::Connection& sourceConn, + SQLite::Connection& baselineConn, + std::string_view tableName, + std::string_view valueName, + SQLite::rowid_t packageRowid, + SQLite::rowid_t& nextNewDataRowid) + { + std::string deltaDataTable = "delta_" + std::string(tableName); + std::string deltaMapTable = "delta_" + std::string(tableName) + "_map"; + std::string mapTable = std::string(tableName) + "_map"; + + // Get current values via join (tags2_map JOIN tags2) + std::vector currentValues; + { + std::string sql = "SELECT t." + std::string(valueName) + + " FROM " + mapTable + " m JOIN " + std::string(tableName) + " t ON m." + std::string(valueName) + " = t.rowid" + + " WHERE m.package = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(sourceConn, sql); + stmt.Bind(1, packageRowid); + while (stmt.Step()) + { + currentValues.push_back(stmt.GetColumn(0)); + } + } + + // Get baseline values via join + std::vector baselineValues; + { + std::string sql = "SELECT t." + std::string(valueName) + + " FROM " + mapTable + " m JOIN " + std::string(tableName) + " t ON m." + std::string(valueName) + " = t.rowid" + + " WHERE m.package = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); + stmt.Bind(1, packageRowid); + while (stmt.Step()) + { + baselineValues.push_back(stmt.GetColumn(0)); + } + } + + // Record added mappings (current but not baseline) + for (const auto& val : currentValues) + { + if (std::find(baselineValues.begin(), baselineValues.end(), val) == baselineValues.end()) + { + SQLite::rowid_t dataRowid = EnsureDeltaDataTableValue( + deltaConn, baselineConn, deltaDataTable, valueName, val, nextNewDataRowid); + + std::string sql = "INSERT OR IGNORE INTO " + deltaMapTable + + " (" + std::string(valueName) + ", package, is_removed) VALUES (?, ?, 0)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, dataRowid); + stmt.Bind(2, packageRowid); + stmt.Execute(); + } + } + + // Record removed mappings (baseline but not current) + for (const auto& val : baselineValues) + { + if (std::find(currentValues.begin(), currentValues.end(), val) == currentValues.end()) + { + // Find the rowid — it's in the baseline data table + SQLite::rowid_t dataRowid = GetBaselineDataTableRowid(baselineConn, std::string(tableName), valueName, val); + if (dataRowid != 0) + { + std::string sql = "INSERT OR IGNORE INTO " + deltaMapTable + + " (" + std::string(valueName) + ", package, is_removed) VALUES (?, ?, 1)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, dataRowid); + stmt.Bind(2, packageRowid); + stmt.Execute(); + } + } + } + } + } + void Interface::EnsureInternalInterface(const SQLite::Connection& connection, bool requireInternalInterface) const { if (!m_internalInterfaceChecked) { - if (!PackagesTable::Exists(connection)) + // In delta read mode the TEMP VIEWs are already set up; no internal interface needed. + if (!m_isDeltaReadMode && !PackagesTable::Exists(connection)) { m_internalInterface = CreateInternalInterface(); } @@ -785,4 +1232,81 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { return CreateISQLiteIndex({ 1, 7 }); } + + void Interface::SetupDeltaReadMode(SQLite::Connection& connection, const std::filesystem::path& baselinePath) + { + AICLI_LOG(Repo, Info, << "Setting up delta read mode with baseline [" << baselinePath << "]"); + + // Attach the baseline database under the "baseline" schema name + { + std::string attachSql = "ATTACH DATABASE ? AS baseline"; + SQLite::Statement stmt = SQLite::Statement::Create(connection, attachSql); + stmt.Bind(1, baselinePath.u8string()); + stmt.Execute(); + } + + // TEMP VIEW: packages + // Delta entries (added/updated) override baseline; removed packages are excluded. + anon::ExecuteSQL(connection, R"( + CREATE TEMP VIEW packages AS + SELECT rowid, id, name, moniker, latest_version, arp_min_version, arp_max_version, hash + FROM delta_packages WHERE is_removed = 0 + UNION ALL + SELECT p.rowid, p.id, p.name, p.moniker, p.latest_version, p.arp_min_version, p.arp_max_version, p.hash + FROM baseline.packages p + WHERE p.id NOT IN (SELECT id FROM delta_packages) + )"); + + // TEMP VIEWs: SystemReference tables (pfns2, productcodes2, norm_names2, norm_publishers2, upgradecodes2) + // For changed packages: delta has the full current set (is_removed=0 = current, is_removed=1 = removed). + // For unchanged packages: baseline rows pass through. + static constexpr std::pair s_SysRefTables[] = { + { "pfns2", "pfn" }, + { "productcodes2", "productcode" }, + { "norm_names2", "norm_name" }, + { "norm_publishers2", "norm_publisher" }, + { "upgradecodes2", "upgradecode" }, + }; + for (const auto& [table, value] : s_SysRefTables) + { + std::string sql = + "CREATE TEMP VIEW " + std::string(table) + " AS " + "SELECT " + std::string(value) + ", package FROM delta_" + std::string(table) + " WHERE is_removed = 0 " + "UNION ALL " + "SELECT " + std::string(value) + ", package FROM baseline." + std::string(table) + " " + "WHERE package NOT IN (SELECT package FROM delta_" + std::string(table) + ")"; + anon::ExecuteSQL(connection, sql); + } + + // TEMP VIEW: tags2 / commands2 data tables (rowid + value). + // Delta only contains NEW strings (with rowids > baseline max); no conflicts possible. + static constexpr std::pair s_OneToManyTables[] = { + { "tags2", "tag" }, + { "commands2", "command" }, + }; + for (const auto& [table, value] : s_OneToManyTables) + { + std::string sql = + "CREATE TEMP VIEW " + std::string(table) + " AS " + "SELECT rowid, " + std::string(value) + " FROM delta_" + std::string(table) + " " + "UNION ALL " + "SELECT rowid, " + std::string(value) + " FROM baseline." + std::string(table); + anon::ExecuteSQL(connection, sql); + + // TEMP VIEW: tags2_map / commands2_map + // For changed packages: delta has the full current set of mappings. + // For unchanged packages: baseline mappings pass through. + std::string mapSql = + "CREATE TEMP VIEW " + std::string(table) + "_map AS " + "SELECT " + std::string(value) + ", package FROM delta_" + std::string(table) + "_map WHERE is_removed = 0 " + "UNION ALL " + "SELECT bm." + std::string(value) + ", bm.package " + "FROM baseline." + std::string(table) + "_map bm " + "WHERE bm.package NOT IN (SELECT package FROM delta_" + std::string(table) + "_map)"; + anon::ExecuteSQL(connection, mapSql); + } + + m_isDeltaReadMode = true; + m_internalInterfaceChecked = true; // Suppress normal EnsureInternalInterface logic + } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp index 1728a5d6e7..bbc89c8fba 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp @@ -16,6 +16,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 static constexpr std::string_view s_PUTT_WriteTime = "write_time"sv; static constexpr std::string_view s_PUTT_Manifest = "manifest"sv; static constexpr std::string_view s_PUTT_Hash = "hash"sv; + static constexpr std::string_view s_PUTT_IsRemoved = "is_removed"sv; std::string_view PackageUpdateTrackingTable::TableName() { @@ -32,8 +33,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 builder.Column(IntegerPrimaryKey()); builder.Column(ColumnBuilder(s_PUTT_Package, Type::Text).NotNull()); builder.Column(ColumnBuilder(s_PUTT_WriteTime, Type::Int64).NotNull()); - builder.Column(ColumnBuilder(s_PUTT_Manifest, Type::Blob).NotNull()); - builder.Column(ColumnBuilder(s_PUTT_Hash, Type::Blob).NotNull()); + builder.Column(ColumnBuilder(s_PUTT_Manifest, Type::Blob)); + builder.Column(ColumnBuilder(s_PUTT_Hash, Type::Blob)); + builder.Column(ColumnBuilder(s_PUTT_IsRemoved, Type::Int64).NotNull().WithDefaultValue(0)); builder.EndColumns(); @@ -84,11 +86,27 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (result.Matches.empty()) { - // Remove any existing package update row - Builder::StatementBuilder deleteBuilder; - deleteBuilder.DeleteFrom(s_PUTT_Table_Name).Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); + // Mark the package as removed rather than deleting the row; clear the data columns. + int64_t currentTime = Utility::GetCurrentUnixEpoch(); + + Builder::StatementBuilder updateBuilder; + updateBuilder.Update(s_PUTT_Table_Name).Set(). + Column(s_PUTT_WriteTime).Equals(currentTime). + Column(s_PUTT_Manifest).Equals(nullptr). + Column(s_PUTT_Hash).Equals(nullptr). + Column(s_PUTT_IsRemoved).Equals(1). + Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); + updateBuilder.Execute(connection); - deleteBuilder.Execute(connection); + if (connection.GetChanges() == 0) + { + // Package was never tracked (added and removed before any tracking checkpoint); record its removal. + Builder::StatementBuilder insertBuilder; + insertBuilder.InsertInto(s_PUTT_Table_Name). + Columns({ s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_IsRemoved }). + Values(packageIdentifier, currentTime, 1); + insertBuilder.Execute(connection); + } } else { @@ -121,11 +139,13 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 int64_t currentTime = Utility::GetCurrentUnixEpoch(); // First attempt to update the row and then insert it if no modification occurred. + // Also clears is_removed in case this package was previously removed and is being re-added. Builder::StatementBuilder updateBuilder; updateBuilder.Update(s_PUTT_Table_Name).Set(). Column(s_PUTT_WriteTime).Equals(currentTime). Column(s_PUTT_Manifest).Equals(compressedManifest). Column(s_PUTT_Hash).Equals(manifestHash). + Column(s_PUTT_IsRemoved).Equals(0). Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); updateBuilder.Execute(connection); @@ -146,9 +166,39 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { bool result = true; - // Ensure that all data in the update table matches the internal index + // Ensure that all non-removed data in the update table matches the internal index for (const PackageData& packageData : GetUpdatesSince(connection, 0)) { + if (packageData.IsRemoved) + { + // Removed packages should not be in the internal index + SearchRequest request; + request.Inclusions.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, packageData.PackageIdentifier); + if (!internalIndex->Search(connection, request).Matches.empty()) + { + if (!log) + { + return false; + } + result = false; + AICLI_LOG(Repo, Info, << " [INVALID] value [" << s_PUTT_Package << "] in table [" << s_PUTT_Table_Name << + "] at row [" << packageData.RowID << "]; package [" << packageData.PackageIdentifier << "] is marked removed but still exists in the internal index"); + } + continue; + } + + if (packageData.Manifest.empty()) + { + if (!log) + { + return false; + } + result = false; + AICLI_LOG(Repo, Info, << " [INVALID] value [" << s_PUTT_Manifest << "] in table [" << s_PUTT_Table_Name << + "] at row [" << packageData.RowID << "]; manifest blob is empty for non-removed package"); + continue; + } + auto manifestHash = Utility::SHA256::ComputeHash(packageData.Manifest); if (!Utility::SHA256::AreEqual(packageData.Hash, manifestHash)) { @@ -178,9 +228,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } - // Ensure that all packages in the internal index are present in the update table + // Ensure that all packages in the internal index are present in the update table (as non-removed) Builder::StatementBuilder builder; - builder.Select(Builder::RowCount).From(s_PUTT_Table_Name).Where(s_PUTT_Package).Like(Builder::Unbound).Escape(EscapeCharForLike); + builder.Select(Builder::RowCount).From(s_PUTT_Table_Name). + Where(s_PUTT_Package).Like(Builder::Unbound).Escape(EscapeCharForLike). + And(s_PUTT_IsRemoved).Equals(0); Statement select = builder.Prepare(connection); @@ -203,7 +255,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } result = false; - AICLI_LOG(Repo, Info, << " [INVALID] value [" << packageIdentifier << "] in the internal index was not found in [" << s_PUTT_Table_Name << "]"); + AICLI_LOG(Repo, Info, << " [INVALID] value [" << packageIdentifier << "] in the internal index was not found as a non-removed entry in [" << s_PUTT_Table_Name << "]"); } } @@ -213,7 +265,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 std::vector PackageUpdateTrackingTable::GetUpdatesSince(const SQLite::Connection& connection, int64_t updateBaseTime) { Builder::StatementBuilder builder; - builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash }). + builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash, s_PUTT_IsRemoved }). From(s_PUTT_Table_Name).Where(s_PUTT_WriteTime).IsGreaterThanOrEqualTo(updateBaseTime); Statement select = builder.Prepare(connection); @@ -226,8 +278,13 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 item.RowID = select.GetColumn(0); item.PackageIdentifier = select.GetColumn(1); item.WriteTime = select.GetColumn(2); - item.Manifest = select.GetColumn(3); - item.Hash = select.GetColumn(4); + item.IsRemoved = (select.GetColumn(5) != 0); + + if (!item.IsRemoved) + { + item.Manifest = select.GetColumn(3); + item.Hash = select.GetColumn(4); + } result.emplace_back(std::move(item)); } @@ -238,7 +295,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 SQLite::blob_t PackageUpdateTrackingTable::GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier) { Builder::StatementBuilder builder; - builder.Select(s_PUTT_Hash).From(s_PUTT_Table_Name).Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); + builder.Select(s_PUTT_Hash).From(s_PUTT_Table_Name). + Where(s_PUTT_Package).LikeWithEscape(packageIdentifier). + And(s_PUTT_IsRemoved).Equals(0); Statement select = builder.Prepare(connection); @@ -246,4 +305,26 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return select.GetColumn(0); } + + void PackageUpdateTrackingTable::EnsureIsRemovedColumn(SQLite::Connection& connection) + { + // Use PRAGMA table_info to check whether is_removed already exists. + SQLite::Statement info = SQLite::Statement::Create(connection, "PRAGMA table_info(update_tracking)"); + bool hasColumn = false; + while (info.Step()) + { + if (info.GetColumn(1) == std::string{ s_PUTT_IsRemoved }) + { + hasColumn = true; + break; + } + } + + if (!hasColumn) + { + SQLite::Statement alter = SQLite::Statement::Create(connection, + "ALTER TABLE update_tracking ADD COLUMN is_removed INTEGER NOT NULL DEFAULT 0"); + alter.Execute(); + } + } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h index 1af63d2c0a..b1a758fa52 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h @@ -41,12 +41,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 int64_t WriteTime = 0; SQLite::blob_t Manifest; SQLite::blob_t Hash; + bool IsRemoved = false; }; // Gets the data on updates that have been written since the given base time. + // Includes entries for removed packages (IsRemoved == true). static std::vector GetUpdatesSince(const SQLite::Connection& connection, int64_t updateBaseTime); // Gets the data hash for the given package identifier. static SQLite::blob_t GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier); + + // Adds the is_removed column to the table if it does not already exist. + // Used when migrating from schema 2.0 to 2.1. + static void EnsureIsRemovedColumn(SQLite::Connection& connection); }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.cpp index 1e74c1e69c..2efed377d2 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.cpp @@ -237,6 +237,32 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return connection.GetLastInsertRowID(); } + SQLite::rowid_t PackagesTable::InsertWithRowId(SQLite::Connection& connection, SQLite::rowid_t rowid, const std::vector& values) + { + SQLite::Builder::StatementBuilder builder; + builder.InsertInto(s_PackagesTable_Table_Name).BeginColumns(); + + builder.Column(SQLite::RowIDName); + for (const NameValuePair& value : values) + { + builder.Column(value.Name); + } + + builder.EndColumns().BeginValues(); + + builder.Value(rowid); + for (const NameValuePair& value : values) + { + builder.Value(value.Value); + } + + builder.EndValues(); + + builder.Execute(connection); + + return rowid; + } + bool PackagesTable::ExistsById(const SQLite::Connection& connection, SQLite::rowid_t id) { SQLite::Builder::StatementBuilder builder; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.h index 2de32ec0e8..aec4bdca62 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.h @@ -146,6 +146,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Insert the given values into the table. static SQLite::rowid_t Insert(SQLite::Connection& connection, const std::vector& values); + // Insert the given values into the table at a specific rowid. + static SQLite::rowid_t InsertWithRowId(SQLite::Connection& connection, SQLite::rowid_t rowid, const std::vector& values); + // Gets a value indicating whether the package with rowid exists. static bool ExistsById(const SQLite::Connection& connection, SQLite::rowid_t rowid); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h new file mode 100644 index 0000000000..cbd540c5a7 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/2_0/Interface.h" + +namespace AppInstaller::Repository::Microsoft::Schema::V2_1 +{ + // Interface to schema version 2.1 exposed through ISQLiteIndex. + // Version 2.1 adds the is_removed column to the update_tracking table, + // enabling delta index generation that can represent package removals. + struct Interface : public V2_0::Interface + { + Interface(Utility::NormalizationVersion normVersion = Utility::NormalizationVersion::Initial); + + // Version 1.0 + SQLite::Version GetVersion() const override; + + // Version 2.0 + bool MigrateFrom(SQLite::Connection& connection, const ISQLiteIndex* current) override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp new file mode 100644 index 0000000000..799db58343 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Interface.h" +#include "Microsoft/Schema/2_0/PackageUpdateTrackingTable.h" + +namespace AppInstaller::Repository::Microsoft::Schema::V2_1 +{ + Interface::Interface(Utility::NormalizationVersion normVersion) : V2_0::Interface(normVersion) {} + + SQLite::Version Interface::GetVersion() const + { + return { 2, 1 }; + } + + bool Interface::MigrateFrom(SQLite::Connection& connection, const ISQLiteIndex* current) + { + THROW_HR_IF_NULL(E_POINTER, current); + + auto currentVersion = current->GetVersion(); + + // Migration from 2.0 → 2.1: add the is_removed column to update_tracking. + if (currentVersion.MajorVersion == 2 && currentVersion.MinorVersion == 0) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migrate_from_v2_1"); + V2_0::PackageUpdateTrackingTable::EnsureIsRemovedColumn(connection); + savepoint.Commit(); + return true; + } + + // Fall through to V2_0 migration (handles 1.7 → 2.0 → 2.1 via two-step upgrade). + return V2_0::Interface::MigrateFrom(connection, current); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp index df753a14dc..d335d78480 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp @@ -12,6 +12,7 @@ #include "Microsoft/Schema/1_6/Interface.h" #include "Microsoft/Schema/1_7/Interface.h" #include "Microsoft/Schema/2_0/Interface.h" +#include "Microsoft/Schema/2_1/Interface.h" namespace AppInstaller::Repository::Microsoft::Schema { @@ -45,13 +46,15 @@ namespace AppInstaller::Repository::Microsoft::Schema return versionCreatorMap[std::min(static_cast(version.MinorVersion), versionCreatorMap.size() - 1)](); } - // Version 2.0 is designed solely for minimizing the size of the index for transport. + // Version 2.x is designed solely for minimizing the size of the index for transport. // Unless it is prepared for packaging, it will be identical to a 1.N index. + // Version 2.1 adds is_removed tracking to enable delta index generation. if (version.MajorVersion == 2) { - constexpr std::array(*)(), 1> versionCreatorMap = + constexpr std::array(*)(), 2> versionCreatorMap = { []() { return std::unique_ptr(std::make_unique()); }, + []() { return std::unique_ptr(std::make_unique()); }, }; return versionCreatorMap[std::min(static_cast(version.MinorVersion), versionCreatorMap.size() - 1)](); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/SQLiteIndexContextData.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/SQLiteIndexContextData.h index 46ac29f44c..ee1a324424 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/SQLiteIndexContextData.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/SQLiteIndexContextData.h @@ -13,6 +13,8 @@ namespace AppInstaller::Repository::Microsoft::Schema PackageUpdateTrackingBaseTime, IntermediateFileOutputPath, DatabaseFilePath, + DeltaBaselineIndexPath, + DeltaOutputPath, Max }; @@ -44,6 +46,20 @@ namespace AppInstaller::Repository::Microsoft::Schema using value_t = std::filesystem::path; static constexpr bool SetThroughInterface = false; }; + + template <> + struct PropertyMapping + { + using value_t = std::filesystem::path; + static constexpr bool SetThroughInterface = false; + }; + + template <> + struct PropertyMapping + { + using value_t = std::filesystem::path; + static constexpr bool SetThroughInterface = false; + }; } using SQLiteIndexContextData = EnumBasedVariantMap; diff --git a/src/WinGetUtil/Exports.cpp b/src/WinGetUtil/Exports.cpp index f713a13544..a2de6cd130 100644 --- a/src/WinGetUtil/Exports.cpp +++ b/src/WinGetUtil/Exports.cpp @@ -35,6 +35,8 @@ namespace { case WinGetSQLiteIndexProperty_PackageUpdateTrackingBaseTime: return SQLiteIndex::Property::PackageUpdateTrackingBaseTime; case WinGetSQLiteIndexProperty_IntermediateFileOutputPath: return SQLiteIndex::Property::IntermediateFileOutputPath; + case WinGetSQLiteIndexProperty_DeltaBaselineIndexPath: return SQLiteIndex::Property::DeltaBaselineIndexPath; + case WinGetSQLiteIndexProperty_DeltaOutputPath: return SQLiteIndex::Property::DeltaOutputPath; } THROW_HR(E_INVALIDARG); @@ -122,6 +124,22 @@ extern "C" } CATCH_RETURN() + WINGET_UTIL_API WinGetSQLiteIndexOpenWithBaseline(WINGET_STRING deltaFilePath, WINGET_STRING baselineFilePath, WINGET_SQLITE_INDEX_HANDLE* index) try + { + THROW_HR_IF(E_INVALIDARG, !deltaFilePath); + THROW_HR_IF(E_INVALIDARG, !baselineFilePath); + THROW_HR_IF(E_INVALIDARG, !index); + THROW_HR_IF(E_INVALIDARG, !!*index); + + std::unique_ptr result = std::make_unique( + SQLiteIndex::OpenWithBaseline(ConvertToUTF8(deltaFilePath), ConvertToUTF8(baselineFilePath))); + + *index = static_cast(result.release()); + + return S_OK; + } + CATCH_RETURN() + WINGET_UTIL_API WinGetSQLiteIndexClose(WINGET_SQLITE_INDEX_HANDLE index) try { std::unique_ptr toClose(reinterpret_cast(index)); diff --git a/src/WinGetUtil/WinGetUtil.h b/src/WinGetUtil/WinGetUtil.h index fe60ca77ed..ffb959e895 100644 --- a/src/WinGetUtil/WinGetUtil.h +++ b/src/WinGetUtil/WinGetUtil.h @@ -121,6 +121,12 @@ extern "C" WINGET_STRING filePath, WINGET_SQLITE_INDEX_HANDLE* index); + // Opens an existing delta index combined with its baseline for reading. + WINGET_UTIL_API WinGetSQLiteIndexOpenWithBaseline( + WINGET_STRING deltaFilePath, + WINGET_STRING baselineFilePath, + WINGET_SQLITE_INDEX_HANDLE* index); + // Closes the index. WINGET_UTIL_API WinGetSQLiteIndexClose( WINGET_SQLITE_INDEX_HANDLE index); @@ -135,6 +141,8 @@ extern "C" { WinGetSQLiteIndexProperty_PackageUpdateTrackingBaseTime = 0, WinGetSQLiteIndexProperty_IntermediateFileOutputPath = 1, + WinGetSQLiteIndexProperty_DeltaBaselineIndexPath = 2, + WinGetSQLiteIndexProperty_DeltaOutputPath = 3, }; // Sets the given property on the index. diff --git a/src/WinGetUtilInterop/Api/WinGetFactory.cs b/src/WinGetUtilInterop/Api/WinGetFactory.cs index e0ae8df76c..ab90c12172 100644 --- a/src/WinGetUtilInterop/Api/WinGetFactory.cs +++ b/src/WinGetUtilInterop/Api/WinGetFactory.cs @@ -57,6 +57,20 @@ public IWinGetSQLiteIndex SQLiteIndexOpen(string indexFile) } } + /// + public IWinGetSQLiteIndex SQLiteIndexOpenWithBaseline(string deltaIndexFile, string baselineIndexFile) + { + try + { + WinGetSQLiteIndexOpenWithBaseline(deltaIndexFile, baselineIndexFile, out IntPtr index); + return new WinGetSQLiteIndex(index); + } + catch (Exception e) + { + throw new WinGetSQLiteIndexException(e); + } + } + /// public IWinGetLogging LoggingInit(string indexLogFile) { @@ -160,6 +174,16 @@ public IWinGetInstallerMetadata BeginInstallerMetadataCollection( [DllImport(Constants.DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] private static extern IntPtr WinGetSQLiteIndexOpen(string filePath, out IntPtr index); + /// + /// Opens an existing delta index combined with its baseline for reading. + /// + /// File path of delta index. + /// File path of baseline index. + /// Out handle of the index. + /// HRESULT. + [DllImport(Constants.DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] + private static extern IntPtr WinGetSQLiteIndexOpenWithBaseline(string deltaFilePath, string baselineFilePath, out IntPtr index); + /// /// Initializes the logging infrastructure. /// diff --git a/src/WinGetUtilInterop/Interfaces/IWinGetFactory.cs b/src/WinGetUtilInterop/Interfaces/IWinGetFactory.cs index 8e450ca5de..401719b3f6 100644 --- a/src/WinGetUtilInterop/Interfaces/IWinGetFactory.cs +++ b/src/WinGetUtilInterop/Interfaces/IWinGetFactory.cs @@ -36,6 +36,14 @@ public interface IWinGetFactory /// Instance of IWinGetSQLiteIndex. IWinGetSQLiteIndex SQLiteIndexOpen(string indexFile); + /// + /// Opens a delta index combined with its baseline for reading. + /// + /// Delta index file to open. + /// Baseline index file to attach. + /// Instance of IWinGetSQLiteIndex. + IWinGetSQLiteIndex SQLiteIndexOpenWithBaseline(string deltaIndexFile, string baselineIndexFile); + /// /// Initializes logging. /// diff --git a/src/WinGetUtilInterop/Interfaces/IWinGetSQLiteIndex.cs b/src/WinGetUtilInterop/Interfaces/IWinGetSQLiteIndex.cs index 15c1e30d6f..e5441fbbfe 100644 --- a/src/WinGetUtilInterop/Interfaces/IWinGetSQLiteIndex.cs +++ b/src/WinGetUtilInterop/Interfaces/IWinGetSQLiteIndex.cs @@ -26,6 +26,18 @@ public enum SQLiteIndexProperty /// The path does not need to exist, and may not be created if no files need to be written. /// IntermediateFileOutputPath = 1, + + /// + /// The full path to the baseline V2 index file to compare against when generating a delta. + /// Must be set together with DeltaOutputPath before calling PrepareForPackaging. + /// + DeltaBaselineIndexPath = 2, + + /// + /// The full path where the delta index file will be written. + /// Must be set together with DeltaBaselineIndexPath before calling PrepareForPackaging. + /// + DeltaOutputPath = 3, } /// diff --git a/tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj b/tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj new file mode 100644 index 0000000000..7d455032a0 --- /dev/null +++ b/tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj @@ -0,0 +1,29 @@ + + + + Exe + net8.0 + $(SolutionDir)$(Platform)\$(Configuration)\DeltaIndexTestTool\ + x64;x86 + enable + enable + + + + + + + + + + Content + PreserveNewest + True + + + + + + + + diff --git a/tools/DeltaIndexTestTool/Program.cs b/tools/DeltaIndexTestTool/Program.cs new file mode 100644 index 0000000000..070e05c4f7 --- /dev/null +++ b/tools/DeltaIndexTestTool/Program.cs @@ -0,0 +1,541 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DeltaIndexTestTool +{ + using LibGit2Sharp; + using Microsoft.WinGetUtil.Api; + using Microsoft.WinGetUtil.Interfaces; + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + + /// + /// Walks the git history of a winget-pkgs clone at weekly intervals, building a full V2 index + /// and a delta index at each checkpoint, then reports the cumulative download size comparison + /// between always-downloading-full vs downloading-baseline-plus-deltas strategies. + /// + class Program + { + static int Main(string[] args) + { + string repoPath = string.Empty; + string outputDir = string.Empty; + int intervalDays = 7; + int maxCheckpoints = 0; + string branch = "master"; + + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--repo" when i + 1 < args.Length: + repoPath = args[++i]; + break; + case "--output" when i + 1 < args.Length: + outputDir = args[++i]; + break; + case "--interval" when i + 1 < args.Length: + intervalDays = int.Parse(args[++i]); + break; + case "--max" when i + 1 < args.Length: + maxCheckpoints = int.Parse(args[++i]); + break; + case "--branch" when i + 1 < args.Length: + branch = args[++i]; + break; + case "--help": + case "-h": + PrintUsage(); + return 0; + } + } + + if (string.IsNullOrEmpty(repoPath) || string.IsNullOrEmpty(outputDir)) + { + PrintUsage(); + return 1; + } + + if (!Directory.Exists(repoPath)) + { + Console.Error.WriteLine($"Repository path does not exist: {repoPath}"); + return 1; + } + + Directory.CreateDirectory(outputDir); + + try + { + RunAnalysis(repoPath, outputDir, branch, intervalDays, maxCheckpoints); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error: {ex.Message}"); + Console.Error.WriteLine(ex.StackTrace); + return 1; + } + } + + static void PrintUsage() + { + Console.WriteLine("DeltaIndexTestTool - Measures delta index size vs full index size over git history"); + Console.WriteLine(); + Console.WriteLine("Usage: DeltaIndexTestTool --repo --output [options]"); + Console.WriteLine(); + Console.WriteLine("Options:"); + Console.WriteLine(" --repo Path to local winget-pkgs git clone"); + Console.WriteLine(" --output Directory to write results and index files"); + Console.WriteLine(" --interval Interval between checkpoints in days (default: 7)"); + Console.WriteLine(" --max Maximum number of checkpoints to process (default: all)"); + Console.WriteLine(" --branch Branch to walk (default: master)"); + Console.WriteLine(); + Console.WriteLine("Output:"); + Console.WriteLine(" results.csv CSV of checkpoint sizes"); + Console.WriteLine(" report.html HTML report with comparison chart"); + } + + static void RunAnalysis(string repoPath, string outputDir, string branch, int intervalDays, int maxCheckpoints) + { + Console.WriteLine($"Opening repository at: {repoPath}"); + Console.WriteLine($"Output directory: {outputDir}"); + Console.WriteLine($"Interval: every {intervalDays} day(s)"); + + var checkpoints = SelectCheckpoints(repoPath, branch, intervalDays, maxCheckpoints); + Console.WriteLine($"Selected {checkpoints.Count} checkpoints"); + + if (checkpoints.Count == 0) + { + Console.Error.WriteLine("No checkpoints found."); + return; + } + + // Working index: maintained in pre-packaging (V1.7) state across builds + string workingIndexPath = Path.Combine(outputDir, "working_index.db"); + + var results = new List(); + + // The WinGet factory for creating indices + var factory = new WinGetFactory(); + + // Handle for the long-lived working index + IWinGetSQLiteIndex? workingIndex = null; + + try + { + for (int i = 0; i < checkpoints.Count; i++) + { + var checkpoint = checkpoints[i]; + Console.WriteLine($"\n[{i + 1}/{checkpoints.Count}] Processing checkpoint: {checkpoint.Commit.Sha[..8]} ({checkpoint.Date:yyyy-MM-dd})"); + + var result = new CheckpointResult + { + Index = i, + Date = checkpoint.Date, + CommitSha = checkpoint.Commit.Sha[..8], + }; + + string checkpointDir = Path.Combine(outputDir, $"checkpoint_{i:D4}"); + Directory.CreateDirectory(checkpointDir); + + string fullIndexPath = Path.Combine(checkpointDir, "full_index.db"); + string deltaPath = Path.Combine(checkpointDir, "delta.db"); + + // --- Build full index and optionally delta --- + + if (i == 0) + { + // First checkpoint: build from scratch + Console.WriteLine(" Building initial full index from scratch..."); + + if (File.Exists(workingIndexPath)) File.Delete(workingIndexPath); + + workingIndex = factory.SQLiteIndexCreate(workingIndexPath, 2u, 1u); + + // Set base time to 0 so all packages are tracked + workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, "0"); + + // Add all YAML manifests at this commit + int added = AddAllManifests(workingIndex, repoPath, checkpoint.Commit, checkpointDir); + Console.WriteLine($" Added {added} manifest files"); + + // Copy working index to produce the full packaged index + workingIndex.Dispose(); + workingIndex = null; + + File.Copy(workingIndexPath, fullIndexPath, overwrite: true); + using (var packagingIndex = factory.SQLiteIndexOpen(fullIndexPath)) + { + packagingIndex.PrepareForPackaging(); + } + + // Re-open working index and set base time to now (track only future changes) + workingIndex = factory.SQLiteIndexOpen(workingIndexPath); + workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); + + result.FullIndexBytes = new FileInfo(fullIndexPath).Length; + result.DeltaBytes = 0; + result.PreviousFullIndexPath = null; + result.FullIndexPath = fullIndexPath; + + Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB"); + } + else + { + // Subsequent checkpoint: apply git diff to working index + var prevCheckpoint = checkpoints[i - 1]; + string prevFullIndexPath = results[i - 1].FullIndexPath!; + + Console.WriteLine(" Applying git diff from previous checkpoint..."); + int changed = ApplyGitDiff(workingIndex!, repoPath, prevCheckpoint.Commit, checkpoint.Commit, checkpointDir); + Console.WriteLine($" Applied {changed} manifest changes"); + + // Copy working index for this checkpoint's packaging + workingIndex!.Dispose(); + workingIndex = null; + + File.Copy(workingIndexPath, fullIndexPath, overwrite: true); + + // Build full index (no delta) from the copy + string fullOnlyPath = fullIndexPath + ".full_only.db"; + File.Copy(workingIndexPath, fullOnlyPath, overwrite: true); + using (var fullPackagingIndex = factory.SQLiteIndexOpen(fullOnlyPath)) + { + fullPackagingIndex.PrepareForPackaging(); + } + // Rename the full-only to fullIndexPath + File.Move(fullOnlyPath, fullIndexPath, overwrite: true); + + // Build delta index against previous full index + string deltaWorkPath = fullIndexPath + ".delta_work.db"; + File.Copy(workingIndexPath, deltaWorkPath, overwrite: true); + using (var deltaPackagingIndex = factory.SQLiteIndexOpen(deltaWorkPath)) + { + deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaBaselineIndexPath, Path.GetFullPath(prevFullIndexPath)); + deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaOutputPath, Path.GetFullPath(deltaPath)); + deltaPackagingIndex.PrepareForPackaging(); + } + File.Delete(deltaWorkPath); + + // Re-open working index and advance base time + workingIndex = factory.SQLiteIndexOpen(workingIndexPath); + workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); + + result.FullIndexBytes = new FileInfo(fullIndexPath).Length; + result.DeltaBytes = File.Exists(deltaPath) ? new FileInfo(deltaPath).Length : 0; + result.PreviousFullIndexPath = prevFullIndexPath; + result.FullIndexPath = fullIndexPath; + + Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB"); + Console.WriteLine($" Delta: {result.DeltaBytes / 1024.0 / 1024.0:F2} MB"); + } + + results.Add(result); + } + } + finally + { + workingIndex?.Dispose(); + } + + // Compute cumulative sizes for strategies + ComputeCumulativeSizes(results); + + // Write CSV + string csvPath = Path.Combine(outputDir, "results.csv"); + WriteCsv(results, csvPath); + Console.WriteLine($"\nResults written to: {csvPath}"); + + // Write HTML report + string htmlPath = Path.Combine(outputDir, "report.html"); + WriteHtmlReport(results, htmlPath); + Console.WriteLine($"Report written to: {htmlPath}"); + } + + /// + /// Selects commits at evenly-spaced intervals across the branch history. + /// + static List SelectCheckpoints(string repoPath, string branch, int intervalDays, int maxCheckpoints) + { + using var repo = new Repository(repoPath); + + var branchRef = repo.Branches[branch] ?? repo.Branches[$"origin/{branch}"]; + if (branchRef == null) + { + throw new InvalidOperationException($"Branch '{branch}' not found in repository"); + } + + // Collect all commits sorted oldest-first + var allCommits = repo.Commits + .QueryBy(new CommitFilter + { + IncludeReachableFrom = branchRef.Tip, + SortBy = CommitSortStrategies.Time | CommitSortStrategies.Reverse, + }) + .ToList(); + + if (allCommits.Count == 0) return []; + + var selected = new List(); + DateTimeOffset? lastSelected = null; + + foreach (var commit in allCommits) + { + var commitTime = commit.Author.When; + + if (lastSelected == null || (commitTime - lastSelected.Value).TotalDays >= intervalDays) + { + selected.Add(new CommitCheckpoint(commit, commitTime.DateTime)); + lastSelected = commitTime; + + if (maxCheckpoints > 0 && selected.Count >= maxCheckpoints) + break; + } + } + + return selected; + } + + /// + /// Extracts all YAML manifest files from a git commit to a temp directory and adds them to the index. + /// Returns the count of manifests added. + /// + static int AddAllManifests(IWinGetSQLiteIndex index, string repoPath, Commit commit, string workDir) + { + string tempDir = Path.Combine(workDir, "manifests_full"); + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + Directory.CreateDirectory(tempDir); + + using var repo = new Repository(repoPath); + var manifestsDir = commit.Tree["manifests"]; + if (manifestsDir == null) return 0; + + int count = ExtractAndAddTree(index, repo, (Tree)manifestsDir.Target, tempDir, "manifests"); + return count; + } + + static int ExtractAndAddTree(IWinGetSQLiteIndex index, Repository repo, Tree tree, string baseDir, string relativePath) + { + int count = 0; + foreach (var entry in tree) + { + string entryRelPath = relativePath + "/" + entry.Name; + if (entry.TargetType == TreeEntryTargetType.Tree) + { + count += ExtractAndAddTree(index, repo, (Tree)entry.Target, baseDir, entryRelPath); + } + else if (entry.TargetType == TreeEntryTargetType.Blob && entry.Name.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) + { + var blob = (Blob)entry.Target; + string localPath = Path.Combine(baseDir, entryRelPath.Replace('/', Path.DirectorySeparatorChar)); + Directory.CreateDirectory(Path.GetDirectoryName(localPath)!); + File.WriteAllBytes(localPath, blob.GetContentStream().ReadAllBytes()); + + try + { + index.AddManifest(localPath, entryRelPath); + count++; + } + catch + { + // Dependency ordering issues — skip for now (same behavior as IndexCreationTool) + } + } + } + return count; + } + + /// + /// Applies the git diff between two commits to the working index. + /// Returns the total number of changed manifest entries. + /// + static int ApplyGitDiff(IWinGetSQLiteIndex index, string repoPath, Commit fromCommit, Commit toCommit, string workDir) + { + string tempDir = Path.Combine(workDir, "manifests_diff"); + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + Directory.CreateDirectory(tempDir); + + using var repo = new Repository(repoPath); + + var diff = repo.Diff.Compare(fromCommit.Tree, toCommit.Tree); + int count = 0; + + foreach (var change in diff) + { + // Only process YAML files under manifests/ + if (!change.Path.StartsWith("manifests/", StringComparison.OrdinalIgnoreCase) || + !change.Path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + try + { + switch (change.Status) + { + case ChangeKind.Added: + { + string localPath = ExtractBlobToTemp(repo, toCommit, change.Path, tempDir); + index.AddManifest(localPath, change.Path); + count++; + break; + } + case ChangeKind.Modified: + case ChangeKind.Renamed: + { + string localPath = ExtractBlobToTemp(repo, toCommit, change.Path, tempDir); + index.UpdateManifest(localPath, change.Path); + count++; + break; + } + case ChangeKind.Deleted: + { + // For removal we need the old content to get the package ID + string localPath = ExtractBlobToTemp(repo, fromCommit, change.OldPath, tempDir); + index.RemoveManifest(localPath, change.OldPath); + count++; + break; + } + } + } + catch + { + // Skip manifest processing errors (e.g., dependency issues) + } + } + + return count; + } + + static string ExtractBlobToTemp(Repository repo, Commit commit, string path, string tempDir) + { + var entry = commit[path]; + var blob = (Blob)entry.Target; + string localPath = Path.Combine(tempDir, path.Replace('/', Path.DirectorySeparatorChar)); + Directory.CreateDirectory(Path.GetDirectoryName(localPath)!); + File.WriteAllBytes(localPath, blob.GetContentStream().ReadAllBytes()); + return localPath; + } + + static void ComputeCumulativeSizes(List results) + { + long cumulativeFull = 0; + long cumulativeDelta = 0; + + for (int i = 0; i < results.Count; i++) + { + cumulativeFull += results[i].FullIndexBytes; + results[i].CumulativeFullDownloadBytes = cumulativeFull; + + if (i == 0) + { + cumulativeDelta += results[i].FullIndexBytes; // First checkpoint: must download full + } + else + { + cumulativeDelta += results[i].DeltaBytes; // Subsequent: download delta only + } + results[i].CumulativeDeltaDownloadBytes = cumulativeDelta; + } + } + + static void WriteCsv(List results, string path) + { + using var writer = new StreamWriter(path, false, Encoding.UTF8); + writer.WriteLine("Index,Date,CommitSha,FullIndexMB,DeltaMB,CumulativeFullMB,CumulativeDeltaMB,SavingsPercent"); + + foreach (var r in results) + { + double fullMb = r.FullIndexBytes / 1024.0 / 1024.0; + double deltaMb = r.DeltaBytes / 1024.0 / 1024.0; + double cumFullMb = r.CumulativeFullDownloadBytes / 1024.0 / 1024.0; + double cumDeltaMb = r.CumulativeDeltaDownloadBytes / 1024.0 / 1024.0; + double savings = r.CumulativeFullDownloadBytes > 0 + ? 100.0 * (1.0 - (double)r.CumulativeDeltaDownloadBytes / r.CumulativeFullDownloadBytes) + : 0; + + writer.WriteLine($"{r.Index},{r.Date:yyyy-MM-dd},{r.CommitSha},{fullMb:F2},{deltaMb:F2},{cumFullMb:F2},{cumDeltaMb:F2},{savings:F1}"); + } + } + + static void WriteHtmlReport(List results, string path) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine("Delta Index Size Analysis"); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("

Delta Index Size Analysis

"); + sb.AppendLine("

Cumulative Download: Full Strategy vs Delta Strategy

"); + sb.AppendLine(""); + sb.AppendLine(""); + + // Summary table + sb.AppendLine("

Per-Checkpoint Details

"); + sb.AppendLine(""); + + foreach (var r in results) + { + double savings = r.CumulativeFullDownloadBytes > 0 + ? 100.0 * (1.0 - (double)r.CumulativeDeltaDownloadBytes / r.CumulativeFullDownloadBytes) + : 0; + + sb.AppendLine($"" + + $"" + + $"" + + $"" + + $"" + + $""); + } + + sb.AppendLine("
IndexDateCommitFull Index (MB)Delta (MB)Cum. Full (MB)Cum. Delta (MB)Savings (%)
{r.Index}{r.Date:yyyy-MM-dd}{r.CommitSha}{r.FullIndexBytes / 1024.0 / 1024.0:F2}{r.DeltaBytes / 1024.0 / 1024.0:F2}{r.CumulativeFullDownloadBytes / 1024.0 / 1024.0:F2}{r.CumulativeDeltaDownloadBytes / 1024.0 / 1024.0:F2}{savings:F1}%
"); + sb.AppendLine(""); + File.WriteAllText(path, sb.ToString(), Encoding.UTF8); + } + } + + record CommitCheckpoint(Commit Commit, DateTime Date); + + class CheckpointResult + { + public int Index { get; set; } + public DateTime Date { get; set; } + public string CommitSha { get; set; } = string.Empty; + public long FullIndexBytes { get; set; } + public long DeltaBytes { get; set; } + public long CumulativeFullDownloadBytes { get; set; } + public long CumulativeDeltaDownloadBytes { get; set; } + public string? FullIndexPath { get; set; } + public string? PreviousFullIndexPath { get; set; } + } + + static class StreamExtensions + { + public static byte[] ReadAllBytes(this Stream stream) + { + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + } +} From adb5778778cc6b0d182aca9c0a64fc4cc6e3bd12 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 13 Mar 2026 14:20:57 -0700 Subject: [PATCH 02/36] Build and tool changes --- .../Microsoft/SQLiteIndex.cpp | 9 +- .../Microsoft/Schema/2_0/Interface_2_0.cpp | 1829 +++++++++-------- .../Schema/2_0/PackageUpdateTrackingTable.cpp | 2 +- .../DeltaIndexTestTool.csproj | 29 +- .../DeltaIndexTestTool/DeltaIndexTestTool.sln | 38 + tools/DeltaIndexTestTool/Program.cs | 177 +- 6 files changed, 1114 insertions(+), 970 deletions(-) create mode 100644 tools/DeltaIndexTestTool/DeltaIndexTestTool.sln diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp index c75ec42487..6911c6757d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -41,17 +41,18 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex SQLiteIndex::OpenWithBaseline(const std::string& deltaFilePath, const std::string& baselineFilePath) { AICLI_LOG(Repo, Info, << "Opening delta index [" << deltaFilePath << "] with baseline [" << baselineFilePath << "]"); - SQLiteIndex result{ deltaFilePath, SQLiteStorageBase::OpenDisposition::ReadOnly }; + SQLiteIndex result{ deltaFilePath, SQLiteStorageBase::OpenDisposition::Read, {} }; std::filesystem::path baselinePath{ Utility::ConvertToUTF16(baselineFilePath) }; THROW_HR_IF(E_INVALIDARG, baselinePath.empty() || baselinePath.is_relative()); result.m_contextData.Add(baselinePath); + // TODO: Add a new interface function for this rather than casting // The interface must be V2_0 to support delta read mode - auto* v2Interface = dynamic_cast(result.m_interface.get()); - THROW_HR_IF(E_NOTIMPL, v2Interface == nullptr); - v2Interface->SetupDeltaReadMode(result.m_dbconn, baselinePath); + //auto* v2Interface = dynamic_cast(result.m_interface.get()); + //THROW_HR_IF(E_NOTIMPL, v2Interface == nullptr); + //v2Interface->SetupDeltaReadMode(result.m_dbconn, baselinePath); return result; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index f449616465..17abad70cf 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -80,1136 +80,1137 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } - Interface::Interface(Utility::NormalizationVersion normVersion) : m_normalizer(normVersion) - { - } - - SQLite::Version Interface::GetVersion() const - { - return { 2, 0 }; - } - - void Interface::CreateTables(SQLite::Connection& connection, CreateOptions options) - { - m_internalInterface = CreateInternalInterface(); - - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createtables_v2_0"); - - // We only create the internal tables at this point, the actual 2.0 tables are created in PrepareForPackaging - m_internalInterface->CreateTables(connection, options); - - savepoint.Commit(); - - m_internalInterfaceChecked = true; - } - - SQLite::rowid_t Interface::AddManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::optional& relativePath) - { - EnsureInternalInterface(connection, true); - SQLite::rowid_t manifestId = m_internalInterface->AddManifest(connection, manifest, relativePath); - PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, manifestId, PackageVersionProperty::Id).value()); - return manifestId; - } - - std::pair Interface::UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::optional& relativePath) - { - EnsureInternalInterface(connection, true); - std::pair result = m_internalInterface->UpdateManifest(connection, manifest, relativePath); - if (result.first) - { - PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, result.second, PackageVersionProperty::Id).value()); - } - return result; - } - - SQLite::rowid_t Interface::RemoveManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest) - { - EnsureInternalInterface(connection, true); - std::optional result = m_internalInterface->GetManifestIdByManifest(connection, manifest); - - // If the manifest doesn't actually exist, fail the remove. - THROW_HR_IF(E_NOT_SET, !result); - - SQLite::rowid_t manifestId = result.value(); - RemoveManifestById(connection, manifestId); - - return manifestId; - } - - void Interface::RemoveManifestById(SQLite::Connection& connection, SQLite::rowid_t manifestId) - { - EnsureInternalInterface(connection, true); - std::optional identifier = m_internalInterface->GetPropertyByPrimaryId(connection, manifestId, PackageVersionProperty::Id); - m_internalInterface->RemoveManifestById(connection, manifestId); - if (identifier) - { - PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), identifier.value()); - } - } - - void Interface::PrepareForPackaging(SQLite::Connection&) - { - // We implement the context version - THROW_HR(E_NOTIMPL); - } - - void Interface::PrepareForPackaging(const SQLiteIndexContext& context) - { - EnsureInternalInterface(context.Connection, true); - PrepareForPackaging(context, true); - } - - bool Interface::CheckConsistency(const SQLite::Connection& connection, bool log) const - { - EnsureInternalInterface(connection); - - bool result = true; - -#define AICLI_CHECK_CONSISTENCY(_check_) \ - if (result || log) \ - { \ - result = _check_ && result; \ - } - - if (m_internalInterface) - { - AICLI_CHECK_CONSISTENCY(m_internalInterface->CheckConsistency(connection, log)); - AICLI_CHECK_CONSISTENCY(PackageUpdateTrackingTable::CheckConsistency(connection, m_internalInterface.get(), log)); - - return result; - } - - AICLI_CHECK_CONSISTENCY((PackagesTable::CheckConsistency< - PackagesTable::IdColumn, - PackagesTable::NameColumn, - PackagesTable::MonikerColumn, - PackagesTable::LatestVersionColumn, - PackagesTable::ARPMinVersionColumn, - PackagesTable::ARPMaxVersionColumn>(connection, log))); - - // Check the 1:N map tables for consistency - AICLI_CHECK_CONSISTENCY(TagsTable::CheckConsistency(connection, log)); - AICLI_CHECK_CONSISTENCY(CommandsTable::CheckConsistency(connection, log)); - - AICLI_CHECK_CONSISTENCY(PackageFamilyNameTable::CheckConsistency(connection, log)); - AICLI_CHECK_CONSISTENCY(ProductCodeTable::CheckConsistency(connection, log)); - AICLI_CHECK_CONSISTENCY(NormalizedPackageNameTable::CheckConsistency(connection, log)); - AICLI_CHECK_CONSISTENCY(NormalizedPackagePublisherTable::CheckConsistency(connection, log)); - AICLI_CHECK_CONSISTENCY(UpgradeCodeTable::CheckConsistency(connection, log)); - -#undef AICLI_CHECK_CONSISTENCY - - return result; - } - - ISQLiteIndex::SearchResult Interface::Search(const SQLite::Connection& connection, const SearchRequest& request) const - { - EnsureInternalInterface(connection); - - if (m_internalInterface) - { - return m_internalInterface->Search(connection, request); - } - - SearchRequest requestCopy = request; - return SearchInternal(connection, requestCopy); - } - - std::optional Interface::GetPropertyByPrimaryId(const SQLite::Connection& connection, SQLite::rowid_t primaryId, PackageVersionProperty property) const - { - EnsureInternalInterface(connection); - - if (m_internalInterface) - { - return m_internalInterface->GetPropertyByPrimaryId(connection, primaryId, property); - } - - switch (property) - { - case PackageVersionProperty::Id: - return PackagesTable::GetValueById(connection, primaryId); - case PackageVersionProperty::Name: - return PackagesTable::GetValueById(connection, primaryId); - case PackageVersionProperty::Version: - return PackagesTable::GetValueById(connection, primaryId); - case PackageVersionProperty::Channel: - return ""; - case PackageVersionProperty::ManifestSHA256Hash: - { - std::optional hash = PackagesTable::GetValueById(connection, primaryId); - return (!hash || hash->empty()) ? std::optional{} : Utility::SHA256::ConvertToString(hash.value()); - } - case PackageVersionProperty::ArpMinVersion: - return PackagesTable::GetValueById(connection, primaryId); - case PackageVersionProperty::ArpMaxVersion: - return PackagesTable::GetValueById(connection, primaryId); - case PackageVersionProperty::Moniker: - return PackagesTable::GetValueById(connection, primaryId); - default: - return {}; - } - } - - std::vector Interface::GetMultiPropertyByPrimaryId(const SQLite::Connection& connection, SQLite::rowid_t primaryId, PackageVersionMultiProperty property) const - { - EnsureInternalInterface(connection); - - if (m_internalInterface) - { - return m_internalInterface->GetMultiPropertyByPrimaryId(connection, primaryId, property); - } - - switch (property) - { - case PackageVersionMultiProperty::PackageFamilyName: - return PackageFamilyNameTable::GetValuesByPrimaryId(connection, primaryId); - case PackageVersionMultiProperty::ProductCode: - return ProductCodeTable::GetValuesByPrimaryId(connection, primaryId); - // These values are not right, as they are normalized. But they are good enough for now and all we have. - case PackageVersionMultiProperty::Name: - return NormalizedPackageNameTable::GetValuesByPrimaryId(connection, primaryId); - case PackageVersionMultiProperty::Publisher: - return NormalizedPackagePublisherTable::GetValuesByPrimaryId(connection, primaryId); - case PackageVersionMultiProperty::UpgradeCode: - return UpgradeCodeTable::GetValuesByPrimaryId(connection, primaryId); - case PackageVersionMultiProperty::Tag: - return TagsTable::GetValuesByPrimaryId(connection, primaryId); - case PackageVersionMultiProperty::Command: - return CommandsTable::GetValuesByPrimaryId(connection, primaryId); - default: - return {}; - } - } - - std::optional Interface::GetManifestIdByKey(const SQLite::Connection& connection, SQLite::rowid_t id, std::string_view version, std::string_view channel) const - { - EnsureInternalInterface(connection); - - if (m_internalInterface) - { - return m_internalInterface->GetManifestIdByKey(connection, id, version, channel); - } - - THROW_HR(E_NOT_VALID_STATE); - } - - std::optional Interface::GetManifestIdByManifest(const SQLite::Connection& connection, const Manifest::Manifest& manifest) const + namespace anon { - EnsureInternalInterface(connection); - - if (m_internalInterface) + // Executes a raw SQL statement on a connection using the statement builder mechanism. + void ExecuteSQL(SQLite::Connection& connection, std::string_view sql) { - return m_internalInterface->GetManifestIdByManifest(connection, manifest); + SQLite::Statement stmt = SQLite::Statement::Create(connection, sql); + stmt.Execute(); } - THROW_HR(E_NOT_VALID_STATE); - } - - std::vector Interface::GetVersionKeysById(const SQLite::Connection& connection, SQLite::rowid_t id) const - { - EnsureInternalInterface(connection); - - if (m_internalInterface) + // Creates all delta tables in the delta connection. + void CreateDeltaSchema(SQLite::Connection& deltaConn) { - return m_internalInterface->GetVersionKeysById(connection, id); - } + ExecuteSQL(deltaConn, R"( + CREATE TABLE IF NOT EXISTS delta_packages ( + rowid INTEGER PRIMARY KEY, + id TEXT NOT NULL, + name TEXT NOT NULL, + moniker TEXT, + latest_version TEXT NOT NULL, + arp_min_version TEXT, + arp_max_version TEXT, + hash BLOB, + is_removed INTEGER NOT NULL DEFAULT 0 + ) + )"); - THROW_HR(E_NOT_VALID_STATE); - } + // SystemReference string tables (value + package_id, no separate id) + static constexpr std::pair s_SysRefTables[] = { + { "pfns2", "pfn" }, + { "productcodes2", "productcode" }, + { "norm_names2", "norm_name" }, + { "norm_publishers2", "norm_publisher" }, + { "upgradecodes2", "upgradecode" }, + }; + for (const auto& [table, value] : s_SysRefTables) + { + std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + + " (" + std::string(value) + " TEXT NOT NULL, package INTEGER NOT NULL, " + + "is_removed INTEGER NOT NULL DEFAULT 0, " + + "PRIMARY KEY (" + std::string(value) + ", package)) WITHOUT ROWID"; + ExecuteSQL(deltaConn, sql); + } - ISQLiteIndex::MetadataResult Interface::GetMetadataByManifestId(const SQLite::Connection&, SQLite::rowid_t) const - { - return {}; - } + // OneToMany data tables (rowid + value) + static constexpr std::pair s_OneToManyTables[] = { + { "tags2", "tag" }, + { "commands2", "command" }, + }; + for (const auto& [table, value] : s_OneToManyTables) + { + std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + + " (rowid INTEGER PRIMARY KEY, " + std::string(value) + " TEXT NOT NULL)"; + ExecuteSQL(deltaConn, sql); + } - void Interface::SetMetadataByManifestId(SQLite::Connection&, SQLite::rowid_t, PackageVersionMetadata, std::string_view) - { - } + // OneToMany map tables (value_rowid + package_rowid) + for (const auto& [table, value] : s_OneToManyTables) + { + std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + "_map" + + " (" + std::string(value) + " INTEGER NOT NULL, package INTEGER NOT NULL, " + + "is_removed INTEGER NOT NULL DEFAULT 0, " + + "PRIMARY KEY (" + std::string(value) + ", package)) WITHOUT ROWID"; + ExecuteSQL(deltaConn, sql); + } + } - Utility::NormalizedName Interface::NormalizeName(std::string_view name, std::string_view publisher) const - { - if (m_internalInterface) + // Returns the rowid of a package in the baseline, or 0 if not found. + SQLite::rowid_t GetBaselinePackageRowid(SQLite::Connection& baselineConn, const std::string& packageId) { - return m_internalInterface->NormalizeName(name, publisher); + SQLite::Builder::StatementBuilder builder; + builder.Select(SQLite::RowIDName).From("packages").Where("id").Equals(packageId); + SQLite::Statement stmt = builder.Prepare(baselineConn); + if (stmt.Step()) + { + return stmt.GetColumn(0); + } + return 0; } - return m_normalizer.Normalize(name, publisher); - } - - std::set> Interface::GetDependenciesByManifestRowId(const SQLite::Connection& connection, SQLite::rowid_t rowid) const - { - EnsureInternalInterface(connection); - - if (m_internalInterface) + // Returns the max rowid in the packages table, or 0 if empty. + SQLite::rowid_t GetMaxPackageRowid(SQLite::Connection& baselineConn) { - return m_internalInterface->GetDependenciesByManifestRowId(connection, rowid); + SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, "SELECT MAX(rowid) FROM packages"); + if (stmt.Step()) + { + // MAX(rowid) returns NULL if table is empty + if (!stmt.GetColumnIsNull(0)) + { + return stmt.GetColumn(0); + } + } + return 0; } - THROW_HR(E_NOT_VALID_STATE); - } - - std::vector> Interface::GetDependentsById(const SQLite::Connection& connection, AppInstaller::Manifest::string_t id) const - { - EnsureInternalInterface(connection); - - if (m_internalInterface) + // Returns the max rowid in a data table (tags2 or commands2), or 0 if empty. + SQLite::rowid_t GetMaxDataTableRowid(SQLite::Connection& baselineConn, std::string_view tableName) { - return m_internalInterface->GetDependentsById(connection, id); + std::string sql = "SELECT MAX(rowid) FROM " + std::string(tableName); + SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); + if (stmt.Step() && !stmt.GetColumnIsNull(0)) + { + return stmt.GetColumn(0); + } + return 0; } - THROW_HR(E_NOT_VALID_STATE); - } - - void Interface::DropTables(SQLite::Connection& connection) - { - EnsureInternalInterface(connection); - - if (m_internalInterface) + // Returns the rowid in baseline data table for the given value string, or 0 if not present. + SQLite::rowid_t GetBaselineDataTableRowid(SQLite::Connection& baselineConn, std::string_view tableName, std::string_view valueName, const std::string& value) { - return m_internalInterface->DropTables(connection); + std::string sql = "SELECT rowid FROM " + std::string(tableName) + " WHERE " + std::string(valueName) + " = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); + stmt.Bind(1, value); + if (stmt.Step()) + { + return stmt.GetColumn(0); + } + return 0; } - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "drop_tables_v2_0"); - - PackagesTable::Drop(connection); + // Inserts or finds a value in delta data table; returns the rowid (possibly from baseline). + // baselineMaxRowid: the starting offset for new delta rowids. + SQLite::rowid_t EnsureDeltaDataTableValue( + SQLite::Connection& deltaConn, + SQLite::Connection& baselineConn, + std::string_view deltaTableName, + std::string_view valueName, + const std::string& value, + SQLite::rowid_t& nextNewRowid) + { + // Check if the value is already in the baseline + SQLite::rowid_t baselineRowid = GetBaselineDataTableRowid(baselineConn, std::string(deltaTableName).substr(6), valueName, value); + if (baselineRowid != 0) + { + return baselineRowid; + } - TagsTable::Drop(connection); - CommandsTable::Drop(connection); + // Check if already in the delta table + std::string selectSql = "SELECT rowid FROM " + std::string(deltaTableName) + " WHERE " + std::string(valueName) + " = ?"; + SQLite::Statement selectStmt = SQLite::Statement::Create(deltaConn, selectSql); + selectStmt.Bind(1, value); + if (selectStmt.Step()) + { + return selectStmt.GetColumn(0); + } - PackageFamilyNameTable::Drop(connection); - ProductCodeTable::Drop(connection); - NormalizedPackageNameTable::Drop(connection); - NormalizedPackagePublisherTable::Drop(connection); - UpgradeCodeTable::Drop(connection); + // Insert as a new entry + SQLite::rowid_t newRowid = ++nextNewRowid; + std::string insertSql = "INSERT INTO " + std::string(deltaTableName) + " (rowid, " + std::string(valueName) + ") VALUES (?, ?)"; + SQLite::Statement insertStmt = SQLite::Statement::Create(deltaConn, insertSql); + insertStmt.Bind(1, newRowid); + insertStmt.Bind(2, value); + insertStmt.Execute(); + return newRowid; + } - savepoint.Commit(); - } + // Processes a SystemReference table for a changed package. + // Compares current values vs baseline values and records adds/removes. + void ProcessDeltaSysRefTable( + SQLite::Connection& deltaConn, + SQLite::Connection& sourceConn, + SQLite::Connection& baselineConn, + std::string_view tableName, + std::string_view valueName, + SQLite::rowid_t packageRowid, + const std::string& packageId) + { + UNREFERENCED_PARAMETER(packageId); + std::string deltaTable = "delta_" + std::string(tableName); + std::string primaryCol = "package"; - bool Interface::MigrateFrom(SQLite::Connection& connection, const ISQLiteIndex* current) - { - THROW_HR_IF_NULL(E_POINTER, current); + // Get current values from the new V2 index + std::vector currentValues; + { + std::string sql = "SELECT " + std::string(valueName) + " FROM " + std::string(tableName) + " WHERE " + primaryCol + " = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(sourceConn, sql); + stmt.Bind(1, packageRowid); + while (stmt.Step()) + { + currentValues.push_back(stmt.GetColumn(0)); + } + } - auto currentVersion = current->GetVersion(); - if (currentVersion.MajorVersion != 1 || currentVersion.MinorVersion != 7) - { - return false; - } + // Get baseline values + std::vector baselineValues; + { + std::string sql = "SELECT " + std::string(valueName) + " FROM " + std::string(tableName) + " WHERE " + primaryCol + " = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); + stmt.Bind(1, packageRowid); + while (stmt.Step()) + { + baselineValues.push_back(stmt.GetColumn(0)); + } + } - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migrate_from_v2_0"); + // Find added values (in current but not baseline) + for (const auto& val : currentValues) + { + if (std::find(baselineValues.begin(), baselineValues.end(), val) == baselineValues.end()) + { + std::string sql = "INSERT OR IGNORE INTO " + deltaTable + + " (" + std::string(valueName) + ", " + primaryCol + ", is_removed) VALUES (?, ?, 0)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, val); + stmt.Bind(2, packageRowid); + stmt.Execute(); + } + } - // We only need to insert all of the existing packages into the update tracking table. - PackageUpdateTrackingTable::EnsureExists(connection); - SearchResult allPackages = current->Search(connection, {}); + // Find removed values (in baseline but not current) + for (const auto& val : baselineValues) + { + if (std::find(currentValues.begin(), currentValues.end(), val) == currentValues.end()) + { + std::string sql = "INSERT OR IGNORE INTO " + deltaTable + + " (" + std::string(valueName) + ", " + primaryCol + ", is_removed) VALUES (?, ?, 1)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, val); + stmt.Bind(2, packageRowid); + stmt.Execute(); + } + } + } - for (const auto& packageMatch : allPackages.Matches) + // Processes a OneToMany table for a changed package. + void ProcessDeltaOneToManyTable( + SQLite::Connection& deltaConn, + SQLite::Connection& sourceConn, + SQLite::Connection& baselineConn, + std::string_view tableName, + std::string_view valueName, + SQLite::rowid_t packageRowid, + SQLite::rowid_t& nextNewDataRowid) { - std::vector versionKeys = current->GetVersionKeysById(connection, packageMatch.first); - ISQLiteIndex::VersionKey& latestVersionKey = versionKeys[0]; - PackageUpdateTrackingTable::Update(connection, current, current->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Id).value(), false); - } + std::string deltaDataTable = "delta_" + std::string(tableName); + std::string deltaMapTable = "delta_" + std::string(tableName) + "_map"; + std::string mapTable = std::string(tableName) + "_map"; - savepoint.Commit(); - return true; - } + // Get current values via join (tags2_map JOIN tags2) + std::vector currentValues; + { + std::string sql = "SELECT t." + std::string(valueName) + + " FROM " + mapTable + " m JOIN " + std::string(tableName) + " t ON m." + std::string(valueName) + " = t.rowid" + + " WHERE m.package = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(sourceConn, sql); + stmt.Bind(1, packageRowid); + while (stmt.Step()) + { + currentValues.push_back(stmt.GetColumn(0)); + } + } - void Interface::SetProperty(SQLite::Connection& connection, Property property, const std::string& value) - { - switch (property) - { - case Property::PackageUpdateTrackingBaseTime: - { - int64_t baseTime = 0; - if (value.empty()) + // Get baseline values via join + std::vector baselineValues; { - baseTime = Utility::GetCurrentUnixEpoch(); + std::string sql = "SELECT t." + std::string(valueName) + + " FROM " + mapTable + " m JOIN " + std::string(tableName) + " t ON m." + std::string(valueName) + " = t.rowid" + + " WHERE m.package = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); + stmt.Bind(1, packageRowid); + while (stmt.Step()) + { + baselineValues.push_back(stmt.GetColumn(0)); + } + } + + // Record added mappings (current but not baseline) + for (const auto& val : currentValues) + { + if (std::find(baselineValues.begin(), baselineValues.end(), val) == baselineValues.end()) + { + SQLite::rowid_t dataRowid = EnsureDeltaDataTableValue( + deltaConn, baselineConn, deltaDataTable, valueName, val, nextNewDataRowid); + + std::string sql = "INSERT OR IGNORE INTO " + deltaMapTable + + " (" + std::string(valueName) + ", package, is_removed) VALUES (?, ?, 0)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, dataRowid); + stmt.Bind(2, packageRowid); + stmt.Execute(); + } } - else + + // Record removed mappings (baseline but not current) + for (const auto& val : baselineValues) { - baseTime = std::stoll(value); + if (std::find(currentValues.begin(), currentValues.end(), val) == currentValues.end()) + { + // Find the rowid — it's in the baseline data table + SQLite::rowid_t dataRowid = GetBaselineDataTableRowid(baselineConn, std::string(tableName), valueName, val); + if (dataRowid != 0) + { + std::string sql = "INSERT OR IGNORE INTO " + deltaMapTable + + " (" + std::string(valueName) + ", package, is_removed) VALUES (?, ?, 1)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, dataRowid); + stmt.Bind(2, packageRowid); + stmt.Execute(); + } + } } - SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_PackageUpdateTrackingBaseTime, std::to_string(baseTime)); } - break; + } - default: - THROW_WIN32(ERROR_NOT_SUPPORTED); - } + Interface::Interface(Utility::NormalizationVersion normVersion) : m_normalizer(normVersion) + { } - std::unique_ptr Interface::CreateSearchResultsTable(const SQLite::Connection& connection) const + SQLite::Version Interface::GetVersion() const { - return std::make_unique(connection); + return { 2, 0 }; } - void Interface::PerformQuerySearch(SearchResultsTable& resultsTable, const RequestMatch& query) const + void Interface::CreateTables(SQLite::Connection& connection, CreateOptions options) { - // First, do an exact match search for the folded system reference strings - // We do this first because it is exact, and likely won't match anything else if it matches this. - PackageMatchFilter filter(PackageMatchField::Unknown, MatchType::Exact, Utility::FoldCase(query.Value)); + m_internalInterface = CreateInternalInterface(); - for (PackageMatchField field : { PackageMatchField::PackageFamilyName, PackageMatchField::ProductCode, PackageMatchField::UpgradeCode }) - { - filter.Field = field; - resultsTable.SearchOnField(filter); - } + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createtables_v2_0"); - // Now search on the unfolded value - filter.Value = query.Value; + // We only create the internal tables at this point, the actual 2.0 tables are created in PrepareForPackaging + m_internalInterface->CreateTables(connection, options); - for (MatchType match : GetDefaultMatchTypeOrder(query.Type)) - { - filter.Type = match; + savepoint.Commit(); - for (auto field : { PackageMatchField::Id, PackageMatchField::Name, PackageMatchField::Moniker, PackageMatchField::Command, PackageMatchField::Tag }) - { - filter.Field = field; - resultsTable.SearchOnField(filter); - } - } + m_internalInterfaceChecked = true; } - OneToManyTableSchema Interface::GetOneToManyTableSchema() const + SQLite::rowid_t Interface::AddManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::optional& relativePath) { - return OneToManyTableSchema::Version_2_0; + EnsureInternalInterface(connection, true); + SQLite::rowid_t manifestId = m_internalInterface->AddManifest(connection, manifest, relativePath); + PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, manifestId, PackageVersionProperty::Id).value()); + return manifestId; } - ISQLiteIndex::SearchResult Interface::SearchInternal(const SQLite::Connection& connection, SearchRequest& request) const + std::pair Interface::UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::optional& relativePath) { - anon::FoldPackageMatchFilters(request.Inclusions); - anon::FoldPackageMatchFilters(request.Filters); - - if (request.Purpose == SearchPurpose::CorrelationToInstalled) + EnsureInternalInterface(connection, true); + std::pair result = m_internalInterface->UpdateManifest(connection, manifest, relativePath); + if (result.first) { - // Correlate from available package to installed package - // For available package to installed package mapping, only one try is needed. - // For example, if ARP DisplayName contains arch, then the installed package's ARP DisplayName should also include arch. - auto candidateInclusionsWithArch = request.Inclusions; - if (anon::UpdatePackageMatchFilters(candidateInclusionsWithArch, m_normalizer, Utility::NormalizationField::Architecture)) - { - // If DisplayNames contain arch, only use Inclusions with arch for search - request.Inclusions = candidateInclusionsWithArch; - } - else - { - // Otherwise, just update the Inclusions with normalization - anon::UpdatePackageMatchFilters(request.Inclusions, m_normalizer); - } - - return BasicSearchInternal(connection, request); + PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, result.second, PackageVersionProperty::Id).value()); } - else if (request.Purpose == SearchPurpose::CorrelationToAvailable) - { - // For installed package to available package correlation, - // try the search with NormalizedName with Arch first, if not found, try with all values. - // This can be extended in the future for more granular search requests. - std::vector candidateSearches; - auto candidateSearchWithArch = request; - if (anon::UpdatePackageMatchFilters(candidateSearchWithArch.Inclusions, m_normalizer, Utility::NormalizationField::Architecture)) - { - candidateSearches.emplace_back(std::move(candidateSearchWithArch)); - } - anon::UpdatePackageMatchFilters(request.Inclusions, m_normalizer); - candidateSearches.emplace_back(request); + return result; + } - SearchResult result; - for (auto& candidateSearch : candidateSearches) - { - result = BasicSearchInternal(connection, candidateSearch); - if (!result.Matches.empty()) - { - break; - } - } + SQLite::rowid_t Interface::RemoveManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest) + { + EnsureInternalInterface(connection, true); + std::optional result = m_internalInterface->GetManifestIdByManifest(connection, manifest); - return result; - } - else - { - anon::UpdatePackageMatchFilters(request.Inclusions, m_normalizer); - anon::UpdatePackageMatchFilters(request.Filters, m_normalizer); + // If the manifest doesn't actually exist, fail the remove. + THROW_HR_IF(E_NOT_SET, !result); - return BasicSearchInternal(connection, request); - } + SQLite::rowid_t manifestId = result.value(); + RemoveManifestById(connection, manifestId); + + return manifestId; } - ISQLiteIndex::SearchResult Interface::BasicSearchInternal(const SQLite::Connection& connection, const SearchRequest& request) const + void Interface::RemoveManifestById(SQLite::Connection& connection, SQLite::rowid_t manifestId) { - if (request.IsForEverything()) + EnsureInternalInterface(connection, true); + std::optional identifier = m_internalInterface->GetPropertyByPrimaryId(connection, manifestId, PackageVersionProperty::Id); + m_internalInterface->RemoveManifestById(connection, manifestId); + if (identifier) { - std::vector ids = PackagesTable::GetAllRowIds(connection, PackagesTable::IdColumn::Name, request.MaximumResults); - - SearchResult result; - for (SQLite::rowid_t id : ids) - { - result.Matches.emplace_back(std::make_pair(id, PackageMatchFilter(PackageMatchField::Id, MatchType::Wildcard))); - } - - result.Truncated = (request.MaximumResults && PackagesTable::GetCount(connection) > request.MaximumResults); - - return result; + PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), identifier.value()); } + } - // First phase, create the search results table and populate it with the initial results. - // If the Query is provided, we search across many fields and put results in together. - // If Inclusions has fields, we add these to the data. - // If neither is defined, we take the first filter and use it as the initial results search. - std::unique_ptr resultsTable = CreateSearchResultsTable(connection); - bool inclusionsAttempted = false; + void Interface::PrepareForPackaging(SQLite::Connection&) + { + // We implement the context version + THROW_HR(E_NOTIMPL); + } - if (request.Query) - { - // Perform searches across multiple tables to populate the initial results. - PerformQuerySearch(*resultsTable.get(), request.Query.value()); + void Interface::PrepareForPackaging(const SQLiteIndexContext& context) + { + EnsureInternalInterface(context.Connection, true); + PrepareForPackaging(context, true); + } - inclusionsAttempted = true; - } + bool Interface::CheckConsistency(const SQLite::Connection& connection, bool log) const + { + EnsureInternalInterface(connection); - if (!request.Inclusions.empty()) - { - for (auto include : request.Inclusions) - { - for (MatchType match : GetDefaultMatchTypeOrder(include.Type)) - { - include.Type = match; - resultsTable->SearchOnField(include); - } - } + bool result = true; - inclusionsAttempted = true; +#define AICLI_CHECK_CONSISTENCY(_check_) \ + if (result || log) \ + { \ + result = _check_ && result; \ } - size_t filterIndex = 0; - if (!inclusionsAttempted) + if (m_internalInterface) { - THROW_HR_IF(E_UNEXPECTED, request.Filters.empty()); + AICLI_CHECK_CONSISTENCY(m_internalInterface->CheckConsistency(connection, log)); + AICLI_CHECK_CONSISTENCY(PackageUpdateTrackingTable::CheckConsistency(connection, m_internalInterface.get(), log)); - // Perform search for just the field matching the first filter - PackageMatchFilter filter = request.Filters[0]; + return result; + } - for (MatchType match : GetDefaultMatchTypeOrder(filter.Type)) - { - filter.Type = match; - resultsTable->SearchOnField(filter); - } + AICLI_CHECK_CONSISTENCY((PackagesTable::CheckConsistency< + PackagesTable::IdColumn, + PackagesTable::NameColumn, + PackagesTable::MonikerColumn, + PackagesTable::LatestVersionColumn, + PackagesTable::ARPMinVersionColumn, + PackagesTable::ARPMaxVersionColumn>(connection, log))); - // Skip the filter as we already know everything matches - filterIndex = 1; - } + // Check the 1:N map tables for consistency + AICLI_CHECK_CONSISTENCY(TagsTable::CheckConsistency(connection, log)); + AICLI_CHECK_CONSISTENCY(CommandsTable::CheckConsistency(connection, log)); - // Remove any duplicate manifest entries - resultsTable->RemoveDuplicatePackageRows(); + AICLI_CHECK_CONSISTENCY(PackageFamilyNameTable::CheckConsistency(connection, log)); + AICLI_CHECK_CONSISTENCY(ProductCodeTable::CheckConsistency(connection, log)); + AICLI_CHECK_CONSISTENCY(NormalizedPackageNameTable::CheckConsistency(connection, log)); + AICLI_CHECK_CONSISTENCY(NormalizedPackagePublisherTable::CheckConsistency(connection, log)); + AICLI_CHECK_CONSISTENCY(UpgradeCodeTable::CheckConsistency(connection, log)); - // Second phase, for remaining filters, flag matching search results, then remove unflagged values. - for (size_t i = filterIndex; i < request.Filters.size(); ++i) - { - PackageMatchFilter filter = request.Filters[i]; +#undef AICLI_CHECK_CONSISTENCY - resultsTable->PrepareToFilter(); + return result; + } - for (MatchType match : GetDefaultMatchTypeOrder(filter.Type)) - { - filter.Type = match; - resultsTable->FilterOnField(filter); - } + ISQLiteIndex::SearchResult Interface::Search(const SQLite::Connection& connection, const SearchRequest& request) const + { + EnsureInternalInterface(connection); - resultsTable->CompleteFilter(); + if (m_internalInterface) + { + return m_internalInterface->Search(connection, request); } - return resultsTable->GetSearchResults(request.MaximumResults); + SearchRequest requestCopy = request; + return SearchInternal(connection, requestCopy); } - void Interface::PrepareForPackaging(const SQLiteIndexContext& context, bool vacuum) + std::optional Interface::GetPropertyByPrimaryId(const SQLite::Connection& connection, SQLite::rowid_t primaryId, PackageVersionProperty property) const { - SQLite::Connection& connection = context.Connection; + EnsureInternalInterface(connection); - // Get the base time from metadata - int64_t updateBaseTime = 0; - std::optional updateBaseTimeString = SQLite::MetadataTable::TryGetNamedValue(connection, s_MetadataValueName_PackageUpdateTrackingBaseTime); - if (updateBaseTimeString && !updateBaseTimeString->empty()) + if (m_internalInterface) { - updateBaseTime = std::stoll(updateBaseTimeString.value()); + return m_internalInterface->GetPropertyByPrimaryId(connection, primaryId, property); } - // Get the output directory or use the file path - std::filesystem::path baseOutputDirectory; - - if (context.Data.Contains(Property::IntermediateFileOutputPath)) + switch (property) { - baseOutputDirectory = context.Data.Get(); - } - else if (context.Data.Contains(Property::DatabaseFilePath)) + case PackageVersionProperty::Id: + return PackagesTable::GetValueById(connection, primaryId); + case PackageVersionProperty::Name: + return PackagesTable::GetValueById(connection, primaryId); + case PackageVersionProperty::Version: + return PackagesTable::GetValueById(connection, primaryId); + case PackageVersionProperty::Channel: + return ""; + case PackageVersionProperty::ManifestSHA256Hash: { - baseOutputDirectory = context.Data.Get(); - baseOutputDirectory = baseOutputDirectory.parent_path(); + std::optional hash = PackagesTable::GetValueById(connection, primaryId); + return (!hash || hash->empty()) ? std::optional{} : Utility::SHA256::ConvertToString(hash.value()); + } + case PackageVersionProperty::ArpMinVersion: + return PackagesTable::GetValueById(connection, primaryId); + case PackageVersionProperty::ArpMaxVersion: + return PackagesTable::GetValueById(connection, primaryId); + case PackageVersionProperty::Moniker: + return PackagesTable::GetValueById(connection, primaryId); + default: + return {}; } + } - THROW_WIN32_IF(ERROR_INVALID_STATE, baseOutputDirectory.empty() || baseOutputDirectory.is_relative()); + std::vector Interface::GetMultiPropertyByPrimaryId(const SQLite::Connection& connection, SQLite::rowid_t primaryId, PackageVersionMultiProperty property) const + { + EnsureInternalInterface(connection); - // Output all of the changed package version manifests since the base time to the target location - for (const auto& packageData : PackageUpdateTrackingTable::GetUpdatesSince(connection, updateBaseTime)) + if (m_internalInterface) { - if (packageData.IsRemoved) - { - continue; - } - - std::filesystem::path packageDirectory = baseOutputDirectory / - Manifest::PackageVersionDataManifest::GetRelativeDirectoryPath(packageData.PackageIdentifier, Utility::SHA256::ConvertToString(packageData.Hash)); - - std::filesystem::create_directories(packageDirectory); - - std::filesystem::path manifestPath = packageDirectory / Manifest::PackageVersionDataManifest::VersionManifestCompressedFileName(); - - AICLI_LOG(Repo, Info, << "Writing PackageVersionDataManifest for [" << packageData.PackageIdentifier << "] to [" << manifestPath << "]"); - - std::ofstream stream(manifestPath, std::ios_base::out | std::ios_base::binary | std::ios_base::trunc); - THROW_LAST_ERROR_IF(stream.fail()); - stream.write(reinterpret_cast(packageData.Manifest.data()), packageData.Manifest.size()); - THROW_LAST_ERROR_IF(stream.fail()); - stream.flush(); + return m_internalInterface->GetMultiPropertyByPrimaryId(connection, primaryId, property); } - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "prepareforpackaging_v2_0"); + switch (property) + { + case PackageVersionMultiProperty::PackageFamilyName: + return PackageFamilyNameTable::GetValuesByPrimaryId(connection, primaryId); + case PackageVersionMultiProperty::ProductCode: + return ProductCodeTable::GetValuesByPrimaryId(connection, primaryId); + // These values are not right, as they are normalized. But they are good enough for now and all we have. + case PackageVersionMultiProperty::Name: + return NormalizedPackageNameTable::GetValuesByPrimaryId(connection, primaryId); + case PackageVersionMultiProperty::Publisher: + return NormalizedPackagePublisherTable::GetValuesByPrimaryId(connection, primaryId); + case PackageVersionMultiProperty::UpgradeCode: + return UpgradeCodeTable::GetValuesByPrimaryId(connection, primaryId); + case PackageVersionMultiProperty::Tag: + return TagsTable::GetValuesByPrimaryId(connection, primaryId); + case PackageVersionMultiProperty::Command: + return CommandsTable::GetValuesByPrimaryId(connection, primaryId); + default: + return {}; + } + } - // Create the 2.0 data tables - PackagesTable::Create< - PackagesTable::IdColumn, - PackagesTable::NameColumn, - PackagesTable::MonikerColumn, - PackagesTable::LatestVersionColumn, - PackagesTable::ARPMinVersionColumn, - PackagesTable::ARPMaxVersionColumn, - PackagesTable::HashColumn - >(connection); + std::optional Interface::GetManifestIdByKey(const SQLite::Connection& connection, SQLite::rowid_t id, std::string_view version, std::string_view channel) const + { + EnsureInternalInterface(connection); - TagsTable::Create(connection, GetOneToManyTableSchema()); - CommandsTable::Create(connection, GetOneToManyTableSchema()); + if (m_internalInterface) + { + return m_internalInterface->GetManifestIdByKey(connection, id, version, channel); + } - PackageFamilyNameTable::Create(connection); - ProductCodeTable::Create(connection); - NormalizedPackageNameTable::Create(connection); - NormalizedPackagePublisherTable::Create(connection); - UpgradeCodeTable::Create(connection); + THROW_HR(E_NOT_VALID_STATE); + } - // Copy data from 1.7 tables to 2.0 tables - SearchResult allPackages = m_internalInterface->Search(connection, {}); + std::optional Interface::GetManifestIdByManifest(const SQLite::Connection& connection, const Manifest::Manifest& manifest) const + { + EnsureInternalInterface(connection); - for (const auto& packageMatch : allPackages.Matches) + if (m_internalInterface) { - std::vector versionKeys = m_internalInterface->GetVersionKeysById(connection, packageMatch.first); - ISQLiteIndex::VersionKey& latestVersionKey = versionKeys[0]; - - std::string packageIdentifier = m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Id).value(); - - std::vector packageData{ - { PackagesTable::IdColumn::Name, packageIdentifier }, - { PackagesTable::NameColumn::Name, m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Name).value() }, - { PackagesTable::LatestVersionColumn::Name, latestVersionKey.VersionAndChannel.GetVersion().ToString() }, - }; + return m_internalInterface->GetManifestIdByManifest(connection, manifest); + } - auto addIfPresent = [&](std::string_view name, std::optional&& value) - { - if (value && !value->empty()) - { - packageData.emplace_back(PackagesTable::NameValuePair{ name, std::move(value).value() }); - } - }; + THROW_HR(E_NOT_VALID_STATE); + } - addIfPresent(PackagesTable::MonikerColumn::Name, m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Moniker).value()); - addIfPresent(PackagesTable::ARPMinVersionColumn::Name, m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::ArpMinVersion).value()); - addIfPresent(PackagesTable::ARPMaxVersionColumn::Name, m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::ArpMaxVersion).value()); + std::vector Interface::GetVersionKeysById(const SQLite::Connection& connection, SQLite::rowid_t id) const + { + EnsureInternalInterface(connection); - auto idRowId = V1_0::IdTable::SelectIdByValue(connection, packageIdentifier); - THROW_HR_IF(E_NOT_VALID_STATE, !idRowId); + if (m_internalInterface) + { + return m_internalInterface->GetVersionKeysById(connection, id); + } - SQLite::rowid_t packageId = PackagesTable::InsertWithRowId(connection, idRowId.value(), packageData); + THROW_HR(E_NOT_VALID_STATE); + } - PackagesTable::UpdateValueIdById(connection, packageId, PackageUpdateTrackingTable::GetDataHash(connection, packageIdentifier)); + ISQLiteIndex::MetadataResult Interface::GetMetadataByManifestId(const SQLite::Connection&, SQLite::rowid_t) const + { + return {}; + } - for (const auto& versionKey : versionKeys) - { - TagsTable::EnsureExistsAndInsert(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::Tag), packageId); - CommandsTable::EnsureExistsAndInsert(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::Command), packageId); + void Interface::SetMetadataByManifestId(SQLite::Connection&, SQLite::rowid_t, PackageVersionMetadata, std::string_view) + { + } - PackageFamilyNameTable::EnsureExists(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::PackageFamilyName), packageId); - ProductCodeTable::EnsureExists(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::ProductCode), packageId); - NormalizedPackageNameTable::EnsureExists(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::Name), packageId); - NormalizedPackagePublisherTable::EnsureExists(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::Publisher), packageId); - UpgradeCodeTable::EnsureExists(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::UpgradeCode), packageId); - } + Utility::NormalizedName Interface::NormalizeName(std::string_view name, std::string_view publisher) const + { + if (m_internalInterface) + { + return m_internalInterface->NormalizeName(name, publisher); } - PackagesTable::PrepareForPackaging< - PackagesTable::IdColumn, - PackagesTable::NameColumn, - PackagesTable::MonikerColumn, - PackagesTable::LatestVersionColumn, - PackagesTable::ARPMinVersionColumn, - PackagesTable::ARPMaxVersionColumn, - PackagesTable::HashColumn - >(connection); + return m_normalizer.Normalize(name, publisher); + } - TagsTable::PrepareForPackaging(connection); - CommandsTable::PrepareForPackaging(connection); + std::set> Interface::GetDependenciesByManifestRowId(const SQLite::Connection& connection, SQLite::rowid_t rowid) const + { + EnsureInternalInterface(connection); - // Generate the delta index before dropping the tracking table (which is needed for delta construction). - // Delta generation is triggered by setting DeltaBaselineIndexPath and DeltaOutputPath on the context. - if (context.Data.Contains(Property::DeltaBaselineIndexPath) && - context.Data.Contains(Property::DeltaOutputPath)) + if (m_internalInterface) { - // Delta packaging requires schema 2.1+ (is_removed column in update_tracking). - THROW_WIN32_IF(ERROR_NOT_SUPPORTED, GetVersion().MinorVersion < 1); - - std::filesystem::path baselinePath = context.Data.Get(); - std::filesystem::path deltaOutputPath = context.Data.Get(); - - AICLI_LOG(Repo, Info, << "Generating delta index at [" << deltaOutputPath << "] against baseline [" << baselinePath << "]"); - - int64_t deltaUpdateBaseTime = 0; - std::optional deltaUpdateBaseTimeString = SQLite::MetadataTable::TryGetNamedValue(connection, s_MetadataValueName_PackageUpdateTrackingBaseTime); - if (deltaUpdateBaseTimeString && !deltaUpdateBaseTimeString->empty()) - { - deltaUpdateBaseTime = std::stoll(deltaUpdateBaseTimeString.value()); - } - - auto changedPackages = PackageUpdateTrackingTable::GetUpdatesSince(connection, deltaUpdateBaseTime); - if (changedPackages.empty()) - { - AICLI_LOG(Repo, Info, << "No changed packages found; skipping delta generation"); - } - else - { - SQLite::Connection baselineConn = SQLite::Connection::Create( - baselinePath.u8string(), SQLite::Connection::OpenDisposition::ReadOnly); - - SQLite::Connection deltaConn = SQLite::Connection::Create( - deltaOutputPath.u8string(), SQLite::Connection::OpenDisposition::Create); + return m_internalInterface->GetDependenciesByManifestRowId(connection, rowid); + } - anon::CreateDeltaSchema(deltaConn); + THROW_HR(E_NOT_VALID_STATE); + } - SQLite::rowid_t maxBaselinePackageRowid = anon::GetMaxPackageRowid(baselineConn); - SQLite::rowid_t nextNewPackageRowid = maxBaselinePackageRowid; + std::vector> Interface::GetDependentsById(const SQLite::Connection& connection, AppInstaller::Manifest::string_t id) const + { + EnsureInternalInterface(connection); - SQLite::rowid_t maxBaselineTagsRowid = anon::GetMaxDataTableRowid(baselineConn, "tags2"); - SQLite::rowid_t nextNewTagsRowid = maxBaselineTagsRowid; + if (m_internalInterface) + { + return m_internalInterface->GetDependentsById(connection, id); + } - SQLite::rowid_t maxBaselineCommandsRowid = anon::GetMaxDataTableRowid(baselineConn, "commands2"); - SQLite::rowid_t nextNewCommandsRowid = maxBaselineCommandsRowid; + THROW_HR(E_NOT_VALID_STATE); + } - SQLite::Savepoint deltaSavepoint = SQLite::Savepoint::Create(deltaConn, "delta_build"); + void Interface::DropTables(SQLite::Connection& connection) + { + EnsureInternalInterface(connection); - for (const auto& pkg : changedPackages) - { - SQLite::rowid_t packageRowid = anon::GetBaselinePackageRowid(baselineConn, pkg.PackageIdentifier); + if (m_internalInterface) + { + return m_internalInterface->DropTables(connection); + } - if (pkg.IsRemoved) - { - if (packageRowid == 0) - { - // Package was added and removed within the same tracking window; skip. - continue; - } + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "drop_tables_v2_0"); - AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); + PackagesTable::Drop(connection); - std::string sql = "INSERT OR REPLACE INTO delta_packages (rowid, id, name, latest_version, is_removed) VALUES (?, ?, '', '', 1)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, packageRowid); - stmt.Bind(2, pkg.PackageIdentifier); - stmt.Execute(); - } - else - { - bool isNewPackage = (packageRowid == 0); - if (isNewPackage) - { - packageRowid = ++nextNewPackageRowid; - } + TagsTable::Drop(connection); + CommandsTable::Drop(connection); - AICLI_LOG(Repo, Verbose, << "Delta: recording " << (isNewPackage ? "addition" : "update") << " of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); + PackageFamilyNameTable::Drop(connection); + ProductCodeTable::Drop(connection); + NormalizedPackageNameTable::Drop(connection); + NormalizedPackagePublisherTable::Drop(connection); + UpgradeCodeTable::Drop(connection); - { - std::string sql = "SELECT id, name, moniker, latest_version, arp_min_version, arp_max_version, hash " - "FROM packages WHERE id = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(connection, sql); - stmt.Bind(1, pkg.PackageIdentifier); - THROW_HR_IF(E_NOT_SET, !stmt.Step()); + savepoint.Commit(); + } - std::string id = stmt.GetColumn(0); - std::string name = stmt.GetColumn(1); - std::string moniker = stmt.GetColumnIsNull(2) ? "" : stmt.GetColumn(2); - std::string latestVersion = stmt.GetColumn(3); - std::string arpMin = stmt.GetColumnIsNull(4) ? "" : stmt.GetColumn(4); - std::string arpMax = stmt.GetColumnIsNull(5) ? "" : stmt.GetColumn(5); - SQLite::blob_t hash = stmt.GetColumnIsNull(6) ? SQLite::blob_t{} : stmt.GetColumn(6); + bool Interface::MigrateFrom(SQLite::Connection& connection, const ISQLiteIndex* current) + { + THROW_HR_IF_NULL(E_POINTER, current); - std::string insertSql = - "INSERT OR REPLACE INTO delta_packages " - "(rowid, id, name, moniker, latest_version, arp_min_version, arp_max_version, hash, is_removed) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)"; - SQLite::Statement insertStmt = SQLite::Statement::Create(deltaConn, insertSql); - insertStmt.Bind(1, packageRowid); - insertStmt.Bind(2, id); - insertStmt.Bind(3, name); - if (moniker.empty()) insertStmt.Bind(4, nullptr); else insertStmt.Bind(4, moniker); - insertStmt.Bind(5, latestVersion); - if (arpMin.empty()) insertStmt.Bind(6, nullptr); else insertStmt.Bind(6, arpMin); - if (arpMax.empty()) insertStmt.Bind(7, nullptr); else insertStmt.Bind(7, arpMax); - if (hash.empty()) insertStmt.Bind(8, nullptr); else insertStmt.Bind(8, hash); - insertStmt.Execute(); - } + auto currentVersion = current->GetVersion(); + if (currentVersion.MajorVersion != 1 || currentVersion.MinorVersion != 7) + { + return false; + } - static constexpr std::pair s_DeltaSysRefTables[] = { - { "pfns2", "pfn" }, - { "productcodes2", "productcode" }, - { "norm_names2", "norm_name" }, - { "norm_publishers2", "norm_publisher" }, - { "upgradecodes2", "upgradecode" }, - }; + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migrate_from_v2_0"); - for (const auto& [table, value] : s_DeltaSysRefTables) - { - anon::ProcessDeltaSysRefTable(deltaConn, connection, baselineConn, - table, value, packageRowid, pkg.PackageIdentifier); - } + // We only need to insert all of the existing packages into the update tracking table. + PackageUpdateTrackingTable::EnsureExists(connection); + SearchResult allPackages = current->Search(connection, {}); - anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, - "tags2", "tag", packageRowid, nextNewTagsRowid); - anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, - "commands2", "command", packageRowid, nextNewCommandsRowid); - } - } + for (const auto& packageMatch : allPackages.Matches) + { + std::vector versionKeys = current->GetVersionKeysById(connection, packageMatch.first); + ISQLiteIndex::VersionKey& latestVersionKey = versionKeys[0]; + PackageUpdateTrackingTable::Update(connection, current, current->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Id).value(), false); + } - deltaSavepoint.Commit(); + savepoint.Commit(); + return true; + } - AICLI_LOG(Repo, Info, << "Delta index generation complete"); + void Interface::SetProperty(SQLite::Connection& connection, Property property, const std::string& value) + { + switch (property) + { + case Property::PackageUpdateTrackingBaseTime: + { + int64_t baseTime = 0; + if (value.empty()) + { + baseTime = Utility::GetCurrentUnixEpoch(); + } + else + { + baseTime = std::stoll(value); } + SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_PackageUpdateTrackingBaseTime, std::to_string(baseTime)); } + break; - PackageUpdateTrackingTable::Drop(connection); + default: + THROW_WIN32(ERROR_NOT_SUPPORTED); + } + } - // The tables based on SystemReferenceStringTable don't need a prepare currently + std::unique_ptr Interface::CreateSearchResultsTable(const SQLite::Connection& connection) const + { + return std::make_unique(connection); + } - // Drop 1.7 tables - m_internalInterface->DropTables(connection); + void Interface::PerformQuerySearch(SearchResultsTable& resultsTable, const RequestMatch& query) const + { + // First, do an exact match search for the folded system reference strings + // We do this first because it is exact, and likely won't match anything else if it matches this. + PackageMatchFilter filter(PackageMatchField::Unknown, MatchType::Exact, Utility::FoldCase(query.Value)); - savepoint.Commit(); + for (PackageMatchField field : { PackageMatchField::PackageFamilyName, PackageMatchField::ProductCode, PackageMatchField::UpgradeCode }) + { + filter.Field = field; + resultsTable.SearchOnField(filter); + } - m_internalInterface.reset(); + // Now search on the unfolded value + filter.Value = query.Value; - if (vacuum) + for (MatchType match : GetDefaultMatchTypeOrder(query.Type)) { - Vacuum(connection); + filter.Type = match; + + for (auto field : { PackageMatchField::Id, PackageMatchField::Name, PackageMatchField::Moniker, PackageMatchField::Command, PackageMatchField::Tag }) + { + filter.Field = field; + resultsTable.SearchOnField(filter); + } } } - void Interface::Vacuum(const SQLite::Connection& connection) + OneToManyTableSchema Interface::GetOneToManyTableSchema() const { - SQLite::Builder::StatementBuilder builder; - builder.Vacuum(); - builder.Execute(connection); + return OneToManyTableSchema::Version_2_0; } - namespace anon + ISQLiteIndex::SearchResult Interface::SearchInternal(const SQLite::Connection& connection, SearchRequest& request) const { - // Executes a raw SQL statement on a connection using the statement builder mechanism. - void ExecuteSQL(SQLite::Connection& connection, std::string_view sql) + anon::FoldPackageMatchFilters(request.Inclusions); + anon::FoldPackageMatchFilters(request.Filters); + + if (request.Purpose == SearchPurpose::CorrelationToInstalled) { - SQLite::Statement stmt = SQLite::Statement::Create(connection, sql); - stmt.Execute(); - } + // Correlate from available package to installed package + // For available package to installed package mapping, only one try is needed. + // For example, if ARP DisplayName contains arch, then the installed package's ARP DisplayName should also include arch. + auto candidateInclusionsWithArch = request.Inclusions; + if (anon::UpdatePackageMatchFilters(candidateInclusionsWithArch, m_normalizer, Utility::NormalizationField::Architecture)) + { + // If DisplayNames contain arch, only use Inclusions with arch for search + request.Inclusions = candidateInclusionsWithArch; + } + else + { + // Otherwise, just update the Inclusions with normalization + anon::UpdatePackageMatchFilters(request.Inclusions, m_normalizer); + } - // Creates all delta tables in the delta connection. - void CreateDeltaSchema(SQLite::Connection& deltaConn) + return BasicSearchInternal(connection, request); + } + else if (request.Purpose == SearchPurpose::CorrelationToAvailable) { - ExecuteSQL(deltaConn, R"( - CREATE TABLE IF NOT EXISTS delta_packages ( - rowid INTEGER PRIMARY KEY, - id TEXT NOT NULL, - name TEXT NOT NULL, - moniker TEXT, - latest_version TEXT NOT NULL, - arp_min_version TEXT, - arp_max_version TEXT, - hash BLOB, - is_removed INTEGER NOT NULL DEFAULT 0 - ) - )"); + // For installed package to available package correlation, + // try the search with NormalizedName with Arch first, if not found, try with all values. + // This can be extended in the future for more granular search requests. + std::vector candidateSearches; + auto candidateSearchWithArch = request; + if (anon::UpdatePackageMatchFilters(candidateSearchWithArch.Inclusions, m_normalizer, Utility::NormalizationField::Architecture)) + { + candidateSearches.emplace_back(std::move(candidateSearchWithArch)); + } + anon::UpdatePackageMatchFilters(request.Inclusions, m_normalizer); + candidateSearches.emplace_back(request); - // SystemReference string tables (value + package_id, no separate id) - static constexpr std::pair s_SysRefTables[] = { - { "pfns2", "pfn" }, - { "productcodes2", "productcode" }, - { "norm_names2", "norm_name" }, - { "norm_publishers2", "norm_publisher" }, - { "upgradecodes2", "upgradecode" }, - }; - for (const auto& [table, value] : s_SysRefTables) + SearchResult result; + for (auto& candidateSearch : candidateSearches) { - std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + - " (" + std::string(value) + " TEXT NOT NULL, package INTEGER NOT NULL, " + - "is_removed INTEGER NOT NULL DEFAULT 0, " + - "PRIMARY KEY (" + std::string(value) + ", package)) WITHOUT ROWID"; - ExecuteSQL(deltaConn, sql); + result = BasicSearchInternal(connection, candidateSearch); + if (!result.Matches.empty()) + { + break; + } } - // OneToMany data tables (rowid + value) - static constexpr std::pair s_OneToManyTables[] = { - { "tags2", "tag" }, - { "commands2", "command" }, - }; - for (const auto& [table, value] : s_OneToManyTables) + return result; + } + else + { + anon::UpdatePackageMatchFilters(request.Inclusions, m_normalizer); + anon::UpdatePackageMatchFilters(request.Filters, m_normalizer); + + return BasicSearchInternal(connection, request); + } + } + + ISQLiteIndex::SearchResult Interface::BasicSearchInternal(const SQLite::Connection& connection, const SearchRequest& request) const + { + if (request.IsForEverything()) + { + std::vector ids = PackagesTable::GetAllRowIds(connection, PackagesTable::IdColumn::Name, request.MaximumResults); + + SearchResult result; + for (SQLite::rowid_t id : ids) { - std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + - " (rowid INTEGER PRIMARY KEY, " + std::string(value) + " TEXT NOT NULL)"; - ExecuteSQL(deltaConn, sql); + result.Matches.emplace_back(std::make_pair(id, PackageMatchFilter(PackageMatchField::Id, MatchType::Wildcard))); } - // OneToMany map tables (value_rowid + package_rowid) - for (const auto& [table, value] : s_OneToManyTables) - { - std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + "_map" + - " (" + std::string(value) + " INTEGER NOT NULL, package INTEGER NOT NULL, " + - "is_removed INTEGER NOT NULL DEFAULT 0, " + - "PRIMARY KEY (" + std::string(value) + ", package)) WITHOUT ROWID"; - ExecuteSQL(deltaConn, sql); - } + result.Truncated = (request.MaximumResults && PackagesTable::GetCount(connection) > request.MaximumResults); + + return result; } - // Returns the rowid of a package in the baseline, or 0 if not found. - SQLite::rowid_t GetBaselinePackageRowid(SQLite::Connection& baselineConn, const std::string& packageId) + // First phase, create the search results table and populate it with the initial results. + // If the Query is provided, we search across many fields and put results in together. + // If Inclusions has fields, we add these to the data. + // If neither is defined, we take the first filter and use it as the initial results search. + std::unique_ptr resultsTable = CreateSearchResultsTable(connection); + bool inclusionsAttempted = false; + + if (request.Query) { - SQLite::Builder::StatementBuilder builder; - builder.Select(SQLite::RowIDName).From("packages").Where("id").Equals(packageId); - SQLite::Statement stmt = builder.Prepare(baselineConn); - if (stmt.Step()) - { - return stmt.GetColumn(0); - } - return 0; + // Perform searches across multiple tables to populate the initial results. + PerformQuerySearch(*resultsTable.get(), request.Query.value()); + + inclusionsAttempted = true; } - // Returns the max rowid in the packages table, or 0 if empty. - SQLite::rowid_t GetMaxPackageRowid(SQLite::Connection& baselineConn) + if (!request.Inclusions.empty()) { - SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, "SELECT MAX(rowid) FROM packages"); - if (stmt.Step()) + for (auto include : request.Inclusions) { - // MAX(rowid) returns NULL if table is empty - if (!stmt.GetColumnIsNull(0)) + for (MatchType match : GetDefaultMatchTypeOrder(include.Type)) { - return stmt.GetColumn(0); + include.Type = match; + resultsTable->SearchOnField(include); } } - return 0; + + inclusionsAttempted = true; } - // Returns the max rowid in a data table (tags2 or commands2), or 0 if empty. - SQLite::rowid_t GetMaxDataTableRowid(SQLite::Connection& baselineConn, std::string_view tableName) + size_t filterIndex = 0; + if (!inclusionsAttempted) { - std::string sql = "SELECT MAX(rowid) FROM " + std::string(tableName); - SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); - if (stmt.Step() && !stmt.GetColumnIsNull(0)) + THROW_HR_IF(E_UNEXPECTED, request.Filters.empty()); + + // Perform search for just the field matching the first filter + PackageMatchFilter filter = request.Filters[0]; + + for (MatchType match : GetDefaultMatchTypeOrder(filter.Type)) { - return stmt.GetColumn(0); + filter.Type = match; + resultsTable->SearchOnField(filter); } - return 0; + + // Skip the filter as we already know everything matches + filterIndex = 1; } - // Returns the rowid in baseline data table for the given value string, or 0 if not present. - SQLite::rowid_t GetBaselineDataTableRowid(SQLite::Connection& baselineConn, std::string_view tableName, std::string_view valueName, const std::string& value) + // Remove any duplicate manifest entries + resultsTable->RemoveDuplicatePackageRows(); + + // Second phase, for remaining filters, flag matching search results, then remove unflagged values. + for (size_t i = filterIndex; i < request.Filters.size(); ++i) { - std::string sql = "SELECT rowid FROM " + std::string(tableName) + " WHERE " + std::string(valueName) + " = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); - stmt.Bind(1, value); - if (stmt.Step()) + PackageMatchFilter filter = request.Filters[i]; + + resultsTable->PrepareToFilter(); + + for (MatchType match : GetDefaultMatchTypeOrder(filter.Type)) { - return stmt.GetColumn(0); + filter.Type = match; + resultsTable->FilterOnField(filter); } - return 0; + + resultsTable->CompleteFilter(); } - // Inserts or finds a value in delta data table; returns the rowid (possibly from baseline). - // baselineMaxRowid: the starting offset for new delta rowids. - SQLite::rowid_t EnsureDeltaDataTableValue( - SQLite::Connection& deltaConn, - SQLite::Connection& baselineConn, - std::string_view deltaTableName, - std::string_view valueName, - const std::string& value, - SQLite::rowid_t& nextNewRowid) + return resultsTable->GetSearchResults(request.MaximumResults); + } + + void Interface::PrepareForPackaging(const SQLiteIndexContext& context, bool vacuum) + { + SQLite::Connection& connection = context.Connection; + + // Get the base time from metadata + int64_t updateBaseTime = 0; + std::optional updateBaseTimeString = SQLite::MetadataTable::TryGetNamedValue(connection, s_MetadataValueName_PackageUpdateTrackingBaseTime); + if (updateBaseTimeString && !updateBaseTimeString->empty()) { - // Check if the value is already in the baseline - SQLite::rowid_t baselineRowid = GetBaselineDataTableRowid(baselineConn, std::string(deltaTableName).substr(6), valueName, value); - if (baselineRowid != 0) - { - return baselineRowid; - } + updateBaseTime = std::stoll(updateBaseTimeString.value()); + } - // Check if already in the delta table - std::string selectSql = "SELECT rowid FROM " + std::string(deltaTableName) + " WHERE " + std::string(valueName) + " = ?"; - SQLite::Statement selectStmt = SQLite::Statement::Create(deltaConn, selectSql); - selectStmt.Bind(1, value); - if (selectStmt.Step()) + // Get the output directory or use the file path + std::filesystem::path baseOutputDirectory; + + if (context.Data.Contains(Property::IntermediateFileOutputPath)) + { + baseOutputDirectory = context.Data.Get(); + } + else if (context.Data.Contains(Property::DatabaseFilePath)) + { + baseOutputDirectory = context.Data.Get(); + baseOutputDirectory = baseOutputDirectory.parent_path(); + } + + THROW_WIN32_IF(ERROR_INVALID_STATE, baseOutputDirectory.empty() || baseOutputDirectory.is_relative()); + + // Output all of the changed package version manifests since the base time to the target location + for (const auto& packageData : PackageUpdateTrackingTable::GetUpdatesSince(connection, updateBaseTime)) + { + if (packageData.IsRemoved) { - return selectStmt.GetColumn(0); + continue; } - // Insert as a new entry - SQLite::rowid_t newRowid = ++nextNewRowid; - std::string insertSql = "INSERT INTO " + std::string(deltaTableName) + " (rowid, " + std::string(valueName) + ") VALUES (?, ?)"; - SQLite::Statement insertStmt = SQLite::Statement::Create(deltaConn, insertSql); - insertStmt.Bind(1, newRowid); - insertStmt.Bind(2, value); - insertStmt.Execute(); - return newRowid; + std::filesystem::path packageDirectory = baseOutputDirectory / + Manifest::PackageVersionDataManifest::GetRelativeDirectoryPath(packageData.PackageIdentifier, Utility::SHA256::ConvertToString(packageData.Hash)); + + std::filesystem::create_directories(packageDirectory); + + std::filesystem::path manifestPath = packageDirectory / Manifest::PackageVersionDataManifest::VersionManifestCompressedFileName(); + + AICLI_LOG(Repo, Info, << "Writing PackageVersionDataManifest for [" << packageData.PackageIdentifier << "] to [" << manifestPath << "]"); + + std::ofstream stream(manifestPath, std::ios_base::out | std::ios_base::binary | std::ios_base::trunc); + THROW_LAST_ERROR_IF(stream.fail()); + stream.write(reinterpret_cast(packageData.Manifest.data()), packageData.Manifest.size()); + THROW_LAST_ERROR_IF(stream.fail()); + stream.flush(); } - // Processes a SystemReference table for a changed package. - // Compares current values vs baseline values and records adds/removes. - void ProcessDeltaSysRefTable( - SQLite::Connection& deltaConn, - SQLite::Connection& sourceConn, - SQLite::Connection& baselineConn, - std::string_view tableName, - std::string_view valueName, - SQLite::rowid_t packageRowid, - const std::string& packageId) + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "prepareforpackaging_v2_0"); + + // Create the 2.0 data tables + PackagesTable::Create< + PackagesTable::IdColumn, + PackagesTable::NameColumn, + PackagesTable::MonikerColumn, + PackagesTable::LatestVersionColumn, + PackagesTable::ARPMinVersionColumn, + PackagesTable::ARPMaxVersionColumn, + PackagesTable::HashColumn + >(connection); + + TagsTable::Create(connection, GetOneToManyTableSchema()); + CommandsTable::Create(connection, GetOneToManyTableSchema()); + + PackageFamilyNameTable::Create(connection); + ProductCodeTable::Create(connection); + NormalizedPackageNameTable::Create(connection); + NormalizedPackagePublisherTable::Create(connection); + UpgradeCodeTable::Create(connection); + + // Copy data from 1.7 tables to 2.0 tables + SearchResult allPackages = m_internalInterface->Search(connection, {}); + + for (const auto& packageMatch : allPackages.Matches) { - std::string deltaTable = "delta_" + std::string(tableName); - std::string primaryCol = "package"; + std::vector versionKeys = m_internalInterface->GetVersionKeysById(connection, packageMatch.first); + ISQLiteIndex::VersionKey& latestVersionKey = versionKeys[0]; - // Get current values from the new V2 index - std::vector currentValues; - { - std::string sql = "SELECT " + std::string(valueName) + " FROM " + std::string(tableName) + " WHERE " + primaryCol + " = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(sourceConn, sql); - stmt.Bind(1, packageRowid); - while (stmt.Step()) + std::string packageIdentifier = m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Id).value(); + + std::vector packageData{ + { PackagesTable::IdColumn::Name, packageIdentifier }, + { PackagesTable::NameColumn::Name, m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Name).value() }, + { PackagesTable::LatestVersionColumn::Name, latestVersionKey.VersionAndChannel.GetVersion().ToString() }, + }; + + auto addIfPresent = [&](std::string_view name, std::optional&& value) { - currentValues.push_back(stmt.GetColumn(0)); - } - } + if (value && !value->empty()) + { + packageData.emplace_back(PackagesTable::NameValuePair{ name, std::move(value).value() }); + } + }; + + addIfPresent(PackagesTable::MonikerColumn::Name, m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Moniker).value()); + addIfPresent(PackagesTable::ARPMinVersionColumn::Name, m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::ArpMinVersion).value()); + addIfPresent(PackagesTable::ARPMaxVersionColumn::Name, m_internalInterface->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::ArpMaxVersion).value()); + + auto idRowId = V1_0::IdTable::SelectIdByValue(connection, packageIdentifier); + THROW_HR_IF(E_NOT_VALID_STATE, !idRowId); + + SQLite::rowid_t packageId = PackagesTable::InsertWithRowId(connection, idRowId.value(), packageData); - // Get baseline values - std::vector baselineValues; - { - std::string sql = "SELECT " + std::string(valueName) + " FROM " + std::string(tableName) + " WHERE " + primaryCol + " = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); - stmt.Bind(1, packageRowid); - while (stmt.Step()) - { - baselineValues.push_back(stmt.GetColumn(0)); - } - } + PackagesTable::UpdateValueIdById(connection, packageId, PackageUpdateTrackingTable::GetDataHash(connection, packageIdentifier)); - // Find added values (in current but not baseline) - for (const auto& val : currentValues) + for (const auto& versionKey : versionKeys) { - if (std::find(baselineValues.begin(), baselineValues.end(), val) == baselineValues.end()) - { - std::string sql = "INSERT OR IGNORE INTO " + deltaTable + - " (" + std::string(valueName) + ", " + primaryCol + ", is_removed) VALUES (?, ?, 0)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, val); - stmt.Bind(2, packageRowid); - stmt.Execute(); - } - } + TagsTable::EnsureExistsAndInsert(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::Tag), packageId); + CommandsTable::EnsureExistsAndInsert(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::Command), packageId); - // Find removed values (in baseline but not current) - for (const auto& val : baselineValues) - { - if (std::find(currentValues.begin(), currentValues.end(), val) == currentValues.end()) - { - std::string sql = "INSERT OR IGNORE INTO " + deltaTable + - " (" + std::string(valueName) + ", " + primaryCol + ", is_removed) VALUES (?, ?, 1)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, val); - stmt.Bind(2, packageRowid); - stmt.Execute(); - } + PackageFamilyNameTable::EnsureExists(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::PackageFamilyName), packageId); + ProductCodeTable::EnsureExists(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::ProductCode), packageId); + NormalizedPackageNameTable::EnsureExists(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::Name), packageId); + NormalizedPackagePublisherTable::EnsureExists(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::Publisher), packageId); + UpgradeCodeTable::EnsureExists(connection, m_internalInterface->GetMultiPropertyByPrimaryId(connection, versionKey.ManifestId, PackageVersionMultiProperty::UpgradeCode), packageId); } } - // Processes a OneToMany table for a changed package. - void ProcessDeltaOneToManyTable( - SQLite::Connection& deltaConn, - SQLite::Connection& sourceConn, - SQLite::Connection& baselineConn, - std::string_view tableName, - std::string_view valueName, - SQLite::rowid_t packageRowid, - SQLite::rowid_t& nextNewDataRowid) + // Generate the delta index before dropping the tracking table (which is needed for delta construction). + // Delta generation is triggered by setting DeltaBaselineIndexPath and DeltaOutputPath on the context. + if (context.Data.Contains(Property::DeltaBaselineIndexPath) && + context.Data.Contains(Property::DeltaOutputPath)) { - std::string deltaDataTable = "delta_" + std::string(tableName); - std::string deltaMapTable = "delta_" + std::string(tableName) + "_map"; - std::string mapTable = std::string(tableName) + "_map"; + // Delta packaging requires schema 2.1+ (is_removed column in update_tracking). + THROW_WIN32_IF(ERROR_NOT_SUPPORTED, GetVersion().MinorVersion < 1); - // Get current values via join (tags2_map JOIN tags2) - std::vector currentValues; + std::filesystem::path baselinePath = context.Data.Get(); + std::filesystem::path deltaOutputPath = context.Data.Get(); + + AICLI_LOG(Repo, Info, << "Generating delta index at [" << deltaOutputPath << "] against baseline [" << baselinePath << "]"); + + int64_t deltaUpdateBaseTime = 0; + std::optional deltaUpdateBaseTimeString = SQLite::MetadataTable::TryGetNamedValue(connection, s_MetadataValueName_PackageUpdateTrackingBaseTime); + if (deltaUpdateBaseTimeString && !deltaUpdateBaseTimeString->empty()) { - std::string sql = "SELECT t." + std::string(valueName) + - " FROM " + mapTable + " m JOIN " + std::string(tableName) + " t ON m." + std::string(valueName) + " = t.rowid" + - " WHERE m.package = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(sourceConn, sql); - stmt.Bind(1, packageRowid); - while (stmt.Step()) - { - currentValues.push_back(stmt.GetColumn(0)); - } + deltaUpdateBaseTime = std::stoll(deltaUpdateBaseTimeString.value()); } - // Get baseline values via join - std::vector baselineValues; + auto changedPackages = PackageUpdateTrackingTable::GetUpdatesSince(connection, deltaUpdateBaseTime); + if (changedPackages.empty()) { - std::string sql = "SELECT t." + std::string(valueName) + - " FROM " + mapTable + " m JOIN " + std::string(tableName) + " t ON m." + std::string(valueName) + " = t.rowid" + - " WHERE m.package = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); - stmt.Bind(1, packageRowid); - while (stmt.Step()) - { - baselineValues.push_back(stmt.GetColumn(0)); - } + AICLI_LOG(Repo, Info, << "No changed packages found; skipping delta generation"); } - - // Record added mappings (current but not baseline) - for (const auto& val : currentValues) + else { - if (std::find(baselineValues.begin(), baselineValues.end(), val) == baselineValues.end()) - { - SQLite::rowid_t dataRowid = EnsureDeltaDataTableValue( - deltaConn, baselineConn, deltaDataTable, valueName, val, nextNewDataRowid); + SQLite::Connection baselineConn = SQLite::Connection::Create( + baselinePath.u8string(), SQLite::Connection::OpenDisposition::ReadOnly); - std::string sql = "INSERT OR IGNORE INTO " + deltaMapTable + - " (" + std::string(valueName) + ", package, is_removed) VALUES (?, ?, 0)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, dataRowid); - stmt.Bind(2, packageRowid); - stmt.Execute(); - } - } + SQLite::Connection deltaConn = SQLite::Connection::Create( + deltaOutputPath.u8string(), SQLite::Connection::OpenDisposition::Create); - // Record removed mappings (baseline but not current) - for (const auto& val : baselineValues) - { - if (std::find(currentValues.begin(), currentValues.end(), val) == currentValues.end()) + anon::CreateDeltaSchema(deltaConn); + + SQLite::rowid_t maxBaselinePackageRowid = anon::GetMaxPackageRowid(baselineConn); + SQLite::rowid_t nextNewPackageRowid = maxBaselinePackageRowid; + + SQLite::rowid_t maxBaselineTagsRowid = anon::GetMaxDataTableRowid(baselineConn, "tags2"); + SQLite::rowid_t nextNewTagsRowid = maxBaselineTagsRowid; + + SQLite::rowid_t maxBaselineCommandsRowid = anon::GetMaxDataTableRowid(baselineConn, "commands2"); + SQLite::rowid_t nextNewCommandsRowid = maxBaselineCommandsRowid; + + SQLite::Savepoint deltaSavepoint = SQLite::Savepoint::Create(deltaConn, "delta_build"); + + for (const auto& pkg : changedPackages) { - // Find the rowid — it's in the baseline data table - SQLite::rowid_t dataRowid = GetBaselineDataTableRowid(baselineConn, std::string(tableName), valueName, val); - if (dataRowid != 0) + SQLite::rowid_t packageRowid = anon::GetBaselinePackageRowid(baselineConn, pkg.PackageIdentifier); + + if (pkg.IsRemoved) { - std::string sql = "INSERT OR IGNORE INTO " + deltaMapTable + - " (" + std::string(valueName) + ", package, is_removed) VALUES (?, ?, 1)"; + if (packageRowid == 0) + { + // Package was added and removed within the same tracking window; skip. + continue; + } + + AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); + + std::string sql = "INSERT OR REPLACE INTO delta_packages (rowid, id, name, latest_version, is_removed) VALUES (?, ?, '', '', 1)"; SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, dataRowid); - stmt.Bind(2, packageRowid); + stmt.Bind(1, packageRowid); + stmt.Bind(2, pkg.PackageIdentifier); stmt.Execute(); } + else + { + bool isNewPackage = (packageRowid == 0); + if (isNewPackage) + { + packageRowid = ++nextNewPackageRowid; + } + + AICLI_LOG(Repo, Verbose, << "Delta: recording " << (isNewPackage ? "addition" : "update") << " of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); + + { + std::string sql = "SELECT id, name, moniker, latest_version, arp_min_version, arp_max_version, hash " + "FROM packages WHERE id = ?"; + SQLite::Statement stmt = SQLite::Statement::Create(connection, sql); + stmt.Bind(1, pkg.PackageIdentifier); + THROW_HR_IF(E_NOT_SET, !stmt.Step()); + + std::string id = stmt.GetColumn(0); + std::string name = stmt.GetColumn(1); + std::string moniker = stmt.GetColumnIsNull(2) ? "" : stmt.GetColumn(2); + std::string latestVersion = stmt.GetColumn(3); + std::string arpMin = stmt.GetColumnIsNull(4) ? "" : stmt.GetColumn(4); + std::string arpMax = stmt.GetColumnIsNull(5) ? "" : stmt.GetColumn(5); + SQLite::blob_t hash = stmt.GetColumnIsNull(6) ? SQLite::blob_t{} : stmt.GetColumn(6); + + std::string insertSql = + "INSERT OR REPLACE INTO delta_packages " + "(rowid, id, name, moniker, latest_version, arp_min_version, arp_max_version, hash, is_removed) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)"; + SQLite::Statement insertStmt = SQLite::Statement::Create(deltaConn, insertSql); + insertStmt.Bind(1, packageRowid); + insertStmt.Bind(2, id); + insertStmt.Bind(3, name); + if (moniker.empty()) insertStmt.Bind(4, nullptr); else insertStmt.Bind(4, moniker); + insertStmt.Bind(5, latestVersion); + if (arpMin.empty()) insertStmt.Bind(6, nullptr); else insertStmt.Bind(6, arpMin); + if (arpMax.empty()) insertStmt.Bind(7, nullptr); else insertStmt.Bind(7, arpMax); + if (hash.empty()) insertStmt.Bind(8, nullptr); else insertStmt.Bind(8, hash); + insertStmt.Execute(); + } + + static constexpr std::pair s_DeltaSysRefTables[] = { + { "pfns2", "pfn" }, + { "productcodes2", "productcode" }, + { "norm_names2", "norm_name" }, + { "norm_publishers2", "norm_publisher" }, + { "upgradecodes2", "upgradecode" }, + }; + + for (const auto& [table, value] : s_DeltaSysRefTables) + { + anon::ProcessDeltaSysRefTable(deltaConn, connection, baselineConn, + table, value, packageRowid, pkg.PackageIdentifier); + } + + anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, + "tags2", "tag", packageRowid, nextNewTagsRowid); + anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, + "commands2", "command", packageRowid, nextNewCommandsRowid); + } } + + deltaSavepoint.Commit(); + + AICLI_LOG(Repo, Info, << "Delta index generation complete"); } } + + PackagesTable::PrepareForPackaging< + PackagesTable::IdColumn, + PackagesTable::NameColumn, + PackagesTable::MonikerColumn, + PackagesTable::LatestVersionColumn, + PackagesTable::ARPMinVersionColumn, + PackagesTable::ARPMaxVersionColumn, + PackagesTable::HashColumn + >(connection); + + TagsTable::PrepareForPackaging(connection); + CommandsTable::PrepareForPackaging(connection); + + PackageUpdateTrackingTable::Drop(connection); + + // The tables based on SystemReferenceStringTable don't need a prepare currently + + // Drop 1.7 tables + m_internalInterface->DropTables(connection); + + savepoint.Commit(); + + m_internalInterface.reset(); + + if (vacuum) + { + Vacuum(connection); + } + } + + void Interface::Vacuum(const SQLite::Connection& connection) + { + SQLite::Builder::StatementBuilder builder; + builder.Vacuum(); + builder.Execute(connection); } void Interface::EnsureInternalInterface(const SQLite::Connection& connection, bool requireInternalInterface) const diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp index bbc89c8fba..66cce9af0f 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp @@ -35,7 +35,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 builder.Column(ColumnBuilder(s_PUTT_WriteTime, Type::Int64).NotNull()); builder.Column(ColumnBuilder(s_PUTT_Manifest, Type::Blob)); builder.Column(ColumnBuilder(s_PUTT_Hash, Type::Blob)); - builder.Column(ColumnBuilder(s_PUTT_IsRemoved, Type::Int64).NotNull().WithDefaultValue(0)); + builder.Column(ColumnBuilder(s_PUTT_IsRemoved, Type::Int64).Default(0).NotNull()); builder.EndColumns(); diff --git a/tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj b/tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj index 7d455032a0..ca1f6c32b0 100644 --- a/tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj +++ b/tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj @@ -3,10 +3,17 @@ Exe net8.0 - $(SolutionDir)$(Platform)\$(Configuration)\DeltaIndexTestTool\ x64;x86 enable enable + + $(MSBuildProjectDirectory)\bin\$(Platform)\$(Configuration)\ + + $(MSBuildThisFileDirectory)..\..\src\$(Platform)\$(Configuration)\WinGetUtil\ @@ -14,16 +21,20 @@ - - - Content - PreserveNewest - True - + - - + + + + diff --git a/tools/DeltaIndexTestTool/DeltaIndexTestTool.sln b/tools/DeltaIndexTestTool/DeltaIndexTestTool.sln new file mode 100644 index 0000000000..709a38fc9b --- /dev/null +++ b/tools/DeltaIndexTestTool/DeltaIndexTestTool.sln @@ -0,0 +1,38 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeltaIndexTestTool", "DeltaIndexTestTool.csproj", "{31C921DB-7C37-4303-894C-A33A3951562B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinGetUtilInterop", "..\..\src\WinGetUtilInterop\WinGetUtilInterop.csproj", "{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}" +EndProject +Global +GlobalSection(SolutionConfigurationPlatforms) = preSolution +Debug|x64 = Debug|x64 +Debug|x86 = Debug|x86 +Release|x64 = Release|x64 +Release|x86 = Release|x86 +EndGlobalSection +GlobalSection(ProjectConfigurationPlatforms) = postSolution +{31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x64.ActiveCfg = Debug|x64 +{31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x64.Build.0 = Debug|x64 +{31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x86.ActiveCfg = Debug|x86 +{31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x86.Build.0 = Debug|x86 +{31C921DB-7C37-4303-894C-A33A3951562B}.Release|x64.ActiveCfg = Release|x64 +{31C921DB-7C37-4303-894C-A33A3951562B}.Release|x64.Build.0 = Release|x64 +{31C921DB-7C37-4303-894C-A33A3951562B}.Release|x86.ActiveCfg = Release|x86 +{31C921DB-7C37-4303-894C-A33A3951562B}.Release|x86.Build.0 = Release|x86 +{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x64.ActiveCfg = Debug|Any CPU +{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x64.Build.0 = Debug|Any CPU +{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x86.ActiveCfg = Debug|Any CPU +{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x86.Build.0 = Debug|Any CPU +{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x64.ActiveCfg = Release|Any CPU +{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x64.Build.0 = Release|Any CPU +{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x86.ActiveCfg = Release|Any CPU +{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x86.Build.0 = Release|Any CPU +EndGlobalSection +GlobalSection(SolutionProperties) = preSolution +HideSolutionNode = FALSE +EndGlobalSection +EndGlobal diff --git a/tools/DeltaIndexTestTool/Program.cs b/tools/DeltaIndexTestTool/Program.cs index 070e05c4f7..6e2f67a208 100644 --- a/tools/DeltaIndexTestTool/Program.cs +++ b/tools/DeltaIndexTestTool/Program.cs @@ -26,6 +26,8 @@ static int Main(string[] args) int intervalDays = 7; int maxCheckpoints = 0; string branch = "master"; + string resumeCommit = string.Empty; + string resumeWorkingIndexPath = string.Empty; for (int i = 0; i < args.Length; i++) { @@ -46,6 +48,12 @@ static int Main(string[] args) case "--branch" when i + 1 < args.Length: branch = args[++i]; break; + case "--resume-commit" when i + 1 < args.Length: + resumeCommit = args[++i]; + break; + case "--resume-working-index" when i + 1 < args.Length: + resumeWorkingIndexPath = args[++i]; + break; case "--help": case "-h": PrintUsage(); @@ -65,11 +73,28 @@ static int Main(string[] args) return 1; } + // Validate resume arguments: both must be provided together + bool hasResume = !string.IsNullOrEmpty(resumeCommit) || !string.IsNullOrEmpty(resumeWorkingIndexPath); + if (hasResume) + { + if (string.IsNullOrEmpty(resumeCommit) || string.IsNullOrEmpty(resumeWorkingIndexPath)) + { + Console.Error.WriteLine("--resume-commit and --resume-working-index must be provided together."); + return 1; + } + if (!File.Exists(resumeWorkingIndexPath)) + { + Console.Error.WriteLine($"Resume working index not found: {resumeWorkingIndexPath}"); + return 1; + } + } + Directory.CreateDirectory(outputDir); try { - RunAnalysis(repoPath, outputDir, branch, intervalDays, maxCheckpoints); + RunAnalysis(repoPath, outputDir, branch, intervalDays, maxCheckpoints, + resumeCommit, resumeWorkingIndexPath); return 0; } catch (Exception ex) @@ -87,24 +112,36 @@ static void PrintUsage() Console.WriteLine("Usage: DeltaIndexTestTool --repo --output [options]"); Console.WriteLine(); Console.WriteLine("Options:"); - Console.WriteLine(" --repo Path to local winget-pkgs git clone"); - Console.WriteLine(" --output Directory to write results and index files"); - Console.WriteLine(" --interval Interval between checkpoints in days (default: 7)"); - Console.WriteLine(" --max Maximum number of checkpoints to process (default: all)"); - Console.WriteLine(" --branch Branch to walk (default: master)"); + Console.WriteLine(" --repo Path to local winget-pkgs git clone"); + Console.WriteLine(" --output Directory to write results and index files"); + Console.WriteLine(" --interval Interval between checkpoints in days (default: 7)"); + Console.WriteLine(" --max Maximum number of checkpoints; selects the N most"); + Console.WriteLine(" recent intervals working backward from HEAD"); + Console.WriteLine(" --branch Branch to walk (default: master)"); + Console.WriteLine(" --resume-commit Commit SHA to resume from (skip initial build)"); + Console.WriteLine(" --resume-working-index Path to pre-packaging working index for resume commit"); + Console.WriteLine(); + Console.WriteLine("Resume: both --resume-* options must be provided together. The tool will"); + Console.WriteLine(" package the working index to produce checkpoint 0's full index, then"); + Console.WriteLine(" continue processing subsequent checkpoints from there."); Console.WriteLine(); Console.WriteLine("Output:"); Console.WriteLine(" results.csv CSV of checkpoint sizes"); Console.WriteLine(" report.html HTML report with comparison chart"); } - static void RunAnalysis(string repoPath, string outputDir, string branch, int intervalDays, int maxCheckpoints) + static void RunAnalysis(string repoPath, string outputDir, string branch, int intervalDays, int maxCheckpoints, + string resumeCommit, string resumeWorkingIndexPath) { Console.WriteLine($"Opening repository at: {repoPath}"); Console.WriteLine($"Output directory: {outputDir}"); Console.WriteLine($"Interval: every {intervalDays} day(s)"); - var checkpoints = SelectCheckpoints(repoPath, branch, intervalDays, maxCheckpoints); + bool isResume = !string.IsNullOrEmpty(resumeCommit); + + // When resuming, find the resume commit to use as anchor; checkpoints start after it. + // When --max is given without resume, work backward from HEAD to select the N most recent intervals. + var checkpoints = SelectCheckpoints(repoPath, branch, intervalDays, maxCheckpoints, isResume ? resumeCommit : null); Console.WriteLine($"Selected {checkpoints.Count} checkpoints"); if (checkpoints.Count == 0) @@ -113,15 +150,9 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in return; } - // Working index: maintained in pre-packaging (V1.7) state across builds string workingIndexPath = Path.Combine(outputDir, "working_index.db"); - var results = new List(); - - // The WinGet factory for creating indices var factory = new WinGetFactory(); - - // Handle for the long-lived working index IWinGetSQLiteIndex? workingIndex = null; try @@ -144,9 +175,32 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in string fullIndexPath = Path.Combine(checkpointDir, "full_index.db"); string deltaPath = Path.Combine(checkpointDir, "delta.db"); - // --- Build full index and optionally delta --- + if (i == 0 && isResume) + { + // Resume: copy the provided working index into position, then package it + // to produce this checkpoint's full index — same as a normal first checkpoint + // except we already have the working state. + Console.WriteLine($" Resuming from provided working index (commit {resumeCommit[..Math.Min(8, resumeCommit.Length)]})"); + + File.Copy(resumeWorkingIndexPath, workingIndexPath, overwrite: true); + + File.Copy(workingIndexPath, fullIndexPath, overwrite: true); + using (var packagingIndex = factory.SQLiteIndexOpen(fullIndexPath)) + { + packagingIndex.PrepareForPackaging(); + } + + workingIndex = factory.SQLiteIndexOpen(workingIndexPath); + workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); + + result.FullIndexBytes = new FileInfo(fullIndexPath).Length; + result.DeltaBytes = 0; + result.PreviousFullIndexPath = null; + result.FullIndexPath = fullIndexPath; - if (i == 0) + Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB"); + } + else if (i == 0) { // First checkpoint: build from scratch Console.WriteLine(" Building initial full index from scratch..."); @@ -154,15 +208,11 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in if (File.Exists(workingIndexPath)) File.Delete(workingIndexPath); workingIndex = factory.SQLiteIndexCreate(workingIndexPath, 2u, 1u); - - // Set base time to 0 so all packages are tracked workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, "0"); - // Add all YAML manifests at this commit int added = AddAllManifests(workingIndex, repoPath, checkpoint.Commit, checkpointDir); Console.WriteLine($" Added {added} manifest files"); - // Copy working index to produce the full packaged index workingIndex.Dispose(); workingIndex = null; @@ -172,7 +222,6 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in packagingIndex.PrepareForPackaging(); } - // Re-open working index and set base time to now (track only future changes) workingIndex = factory.SQLiteIndexOpen(workingIndexPath); workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); @@ -193,20 +242,16 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in int changed = ApplyGitDiff(workingIndex!, repoPath, prevCheckpoint.Commit, checkpoint.Commit, checkpointDir); Console.WriteLine($" Applied {changed} manifest changes"); - // Copy working index for this checkpoint's packaging workingIndex!.Dispose(); workingIndex = null; - File.Copy(workingIndexPath, fullIndexPath, overwrite: true); - - // Build full index (no delta) from the copy + // Build full index (no delta properties set) string fullOnlyPath = fullIndexPath + ".full_only.db"; File.Copy(workingIndexPath, fullOnlyPath, overwrite: true); using (var fullPackagingIndex = factory.SQLiteIndexOpen(fullOnlyPath)) { fullPackagingIndex.PrepareForPackaging(); } - // Rename the full-only to fullIndexPath File.Move(fullOnlyPath, fullIndexPath, overwrite: true); // Build delta index against previous full index @@ -220,7 +265,6 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in } File.Delete(deltaWorkPath); - // Re-open working index and advance base time workingIndex = factory.SQLiteIndexOpen(workingIndexPath); workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); @@ -241,15 +285,12 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in workingIndex?.Dispose(); } - // Compute cumulative sizes for strategies ComputeCumulativeSizes(results); - // Write CSV string csvPath = Path.Combine(outputDir, "results.csv"); WriteCsv(results, csvPath); Console.WriteLine($"\nResults written to: {csvPath}"); - // Write HTML report string htmlPath = Path.Combine(outputDir, "report.html"); WriteHtmlReport(results, htmlPath); Console.WriteLine($"Report written to: {htmlPath}"); @@ -257,8 +298,14 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in /// /// Selects commits at evenly-spaced intervals across the branch history. + /// + /// When is positive, selects the N most recent + /// intervals working backward from HEAD, then returns them in chronological order. + /// + /// When is provided, only commits strictly after + /// that commit are considered (used for resume mode). /// - static List SelectCheckpoints(string repoPath, string branch, int intervalDays, int maxCheckpoints) + static List SelectCheckpoints(string repoPath, string branch, int intervalDays, int maxCheckpoints, string? afterCommitSha) { using var repo = new Repository(repoPath); @@ -268,35 +315,81 @@ static List SelectCheckpoints(string repoPath, string branch, throw new InvalidOperationException($"Branch '{branch}' not found in repository"); } - // Collect all commits sorted oldest-first + // Collect all commits sorted newest-first var allCommits = repo.Commits .QueryBy(new CommitFilter { IncludeReachableFrom = branchRef.Tip, - SortBy = CommitSortStrategies.Time | CommitSortStrategies.Reverse, + SortBy = CommitSortStrategies.Time, }) .ToList(); if (allCommits.Count == 0) return []; - var selected = new List(); - DateTimeOffset? lastSelected = null; + // If resuming, find the anchor commit and exclude it and anything older + DateTimeOffset? afterTime = null; + if (!string.IsNullOrEmpty(afterCommitSha)) + { + var anchor = allCommits.FirstOrDefault(c => c.Sha.StartsWith(afterCommitSha, StringComparison.OrdinalIgnoreCase)); + if (anchor == null) + { + throw new InvalidOperationException($"Resume commit '{afterCommitSha}' not found on branch '{branch}'"); + } + afterTime = anchor.Author.When; + } - foreach (var commit in allCommits) + if (maxCheckpoints > 0) { - var commitTime = commit.Author.When; + // Work backward from HEAD: pick intervals going back in time, then reverse + var selected = new List(); + DateTimeOffset? lastSelected = null; - if (lastSelected == null || (commitTime - lastSelected.Value).TotalDays >= intervalDays) + foreach (var commit in allCommits) // newest-first { - selected.Add(new CommitCheckpoint(commit, commitTime.DateTime)); - lastSelected = commitTime; + var commitTime = commit.Author.When; - if (maxCheckpoints > 0 && selected.Count >= maxCheckpoints) + // Skip commits at or before the resume anchor + if (afterTime.HasValue && commitTime <= afterTime.Value) break; + + if (lastSelected == null || (lastSelected.Value - commitTime).TotalDays >= intervalDays) + { + selected.Add(new CommitCheckpoint(commit, commitTime.DateTime)); + lastSelected = commitTime; + + if (selected.Count >= maxCheckpoints) + break; + } } + + // Return in chronological order (oldest first) + selected.Reverse(); + return selected; } + else + { + // Walk oldest-first, selecting at each interval + var chronological = allCommits + .Where(c => !afterTime.HasValue || c.Author.When > afterTime.Value) + .Reverse() + .ToList(); + + var selected = new List(); + DateTimeOffset? lastSelected = null; - return selected; + foreach (var commit in chronological) + { + var commitTime = commit.Author.When; + + if (lastSelected == null || (commitTime - lastSelected.Value).TotalDays >= intervalDays) + { + selected.Add(new CommitCheckpoint(commit, commitTime.DateTime)); + lastSelected = commitTime; + } + } + + return selected; + } } /// From a1048ceef6e994252a3d384c3443f5edb889ff2b Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 16 Mar 2026 14:41:12 -0700 Subject: [PATCH 03/36] much test iteration --- .../Microsoft/Schema/2_0/Interface.h | 1 + .../Microsoft/Schema/2_0/Interface_2_0.cpp | 15 +- .../SQLiteStatementBuilder.cpp | 6 +- .../DeltaIndexTestTool/DeltaIndexTestTool.sln | 83 +-- tools/DeltaIndexTestTool/Program.cs | 482 ++++++++++++------ 5 files changed, 372 insertions(+), 215 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h index cf2d038eed..33699b982f 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h @@ -14,6 +14,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { // Version 2.0 static constexpr std::string_view s_MetadataValueName_PackageUpdateTrackingBaseTime = "updateTrackingBase"sv; + static constexpr std::string_view s_MetadataValueName_DeltaBaselineTime = "deltaBaselineTime"sv; // Interface to this schema version exposed through ISQLiteIndex. struct Interface : public ISQLiteIndex diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index 17abad70cf..c345a32c5a 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -925,6 +925,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { SQLite::Connection& connection = context.Connection; + // TODO: We may need to set the baseline time to the max update tracking time +1 to only catch new incoming changes + // This assumes some delay between delta generation and the next package update. + // TODO: We also need to ensure that our times are UTC / not impacted by timezone shifts, etc. + SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_DeltaBaselineTime, std::to_string(Utility::GetCurrentUnixEpoch())); + // Get the base time from metadata int64_t updateBaseTime = 0; std::optional updateBaseTimeString = SQLite::MetadataTable::TryGetNamedValue(connection, s_MetadataValueName_PackageUpdateTrackingBaseTime); @@ -948,6 +953,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 THROW_WIN32_IF(ERROR_INVALID_STATE, baseOutputDirectory.empty() || baseOutputDirectory.is_relative()); + // TEMP + PackageUpdateTrackingTable::EnsureExists(connection); + // Output all of the changed package version manifests since the base time to the target location for (const auto& packageData : PackageUpdateTrackingTable::GetUpdatesSince(connection, updateBaseTime)) { @@ -1055,8 +1063,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 AICLI_LOG(Repo, Info, << "Generating delta index at [" << deltaOutputPath << "] against baseline [" << baselinePath << "]"); + SQLite::Connection baselineConn = SQLite::Connection::Create(baselinePath.u8string(), SQLite::Connection::OpenDisposition::ReadOnly); + int64_t deltaUpdateBaseTime = 0; - std::optional deltaUpdateBaseTimeString = SQLite::MetadataTable::TryGetNamedValue(connection, s_MetadataValueName_PackageUpdateTrackingBaseTime); + std::optional deltaUpdateBaseTimeString = SQLite::MetadataTable::TryGetNamedValue(baselineConn, s_MetadataValueName_DeltaBaselineTime); if (deltaUpdateBaseTimeString && !deltaUpdateBaseTimeString->empty()) { deltaUpdateBaseTime = std::stoll(deltaUpdateBaseTimeString.value()); @@ -1069,9 +1079,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } else { - SQLite::Connection baselineConn = SQLite::Connection::Create( - baselinePath.u8string(), SQLite::Connection::OpenDisposition::ReadOnly); - SQLite::Connection deltaConn = SQLite::Connection::Create( deltaOutputPath.u8string(), SQLite::Connection::OpenDisposition::Create); diff --git a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp index a28644025b..d16ff4c38b 100644 --- a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp +++ b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp @@ -346,10 +346,8 @@ namespace AppInstaller::SQLite::Builder StatementBuilder& StatementBuilder::Equals(std::nullptr_t) { - // This is almost certainly not what you want. - // In SQL, value = NULL is always false. - // Use StatementBuilder::IsNull instead. - THROW_HR(E_NOTIMPL); + m_stream << " = NULL"; + return *this; } StatementBuilder& StatementBuilder::Equals() diff --git a/tools/DeltaIndexTestTool/DeltaIndexTestTool.sln b/tools/DeltaIndexTestTool/DeltaIndexTestTool.sln index 709a38fc9b..0c73aeeacb 100644 --- a/tools/DeltaIndexTestTool/DeltaIndexTestTool.sln +++ b/tools/DeltaIndexTestTool/DeltaIndexTestTool.sln @@ -1,38 +1,47 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeltaIndexTestTool", "DeltaIndexTestTool.csproj", "{31C921DB-7C37-4303-894C-A33A3951562B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinGetUtilInterop", "..\..\src\WinGetUtilInterop\WinGetUtilInterop.csproj", "{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}" -EndProject -Global -GlobalSection(SolutionConfigurationPlatforms) = preSolution -Debug|x64 = Debug|x64 -Debug|x86 = Debug|x86 -Release|x64 = Release|x64 -Release|x86 = Release|x86 -EndGlobalSection -GlobalSection(ProjectConfigurationPlatforms) = postSolution -{31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x64.ActiveCfg = Debug|x64 -{31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x64.Build.0 = Debug|x64 -{31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x86.ActiveCfg = Debug|x86 -{31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x86.Build.0 = Debug|x86 -{31C921DB-7C37-4303-894C-A33A3951562B}.Release|x64.ActiveCfg = Release|x64 -{31C921DB-7C37-4303-894C-A33A3951562B}.Release|x64.Build.0 = Release|x64 -{31C921DB-7C37-4303-894C-A33A3951562B}.Release|x86.ActiveCfg = Release|x86 -{31C921DB-7C37-4303-894C-A33A3951562B}.Release|x86.Build.0 = Release|x86 -{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x64.ActiveCfg = Debug|Any CPU -{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x64.Build.0 = Debug|Any CPU -{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x86.ActiveCfg = Debug|Any CPU -{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x86.Build.0 = Debug|Any CPU -{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x64.ActiveCfg = Release|Any CPU -{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x64.Build.0 = Release|Any CPU -{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x86.ActiveCfg = Release|Any CPU -{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x86.Build.0 = Release|Any CPU -EndGlobalSection -GlobalSection(SolutionProperties) = preSolution -HideSolutionNode = FALSE -EndGlobalSection +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11512.155 d18.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeltaIndexTestTool", "DeltaIndexTestTool.csproj", "{31C921DB-7C37-4303-894C-A33A3951562B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinGetUtilInterop", "..\..\src\WinGetUtilInterop\WinGetUtilInterop.csproj", "{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|Any CPU.ActiveCfg = Debug|x64 + {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|Any CPU.Build.0 = Debug|x64 + {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x64.ActiveCfg = Debug|x64 + {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x64.Build.0 = Debug|x64 + {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x86.ActiveCfg = Debug|x86 + {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x86.Build.0 = Debug|x86 + {31C921DB-7C37-4303-894C-A33A3951562B}.Release|Any CPU.ActiveCfg = Release|x64 + {31C921DB-7C37-4303-894C-A33A3951562B}.Release|Any CPU.Build.0 = Release|x64 + {31C921DB-7C37-4303-894C-A33A3951562B}.Release|x64.ActiveCfg = Release|x64 + {31C921DB-7C37-4303-894C-A33A3951562B}.Release|x64.Build.0 = Release|x64 + {31C921DB-7C37-4303-894C-A33A3951562B}.Release|x86.ActiveCfg = Release|x86 + {31C921DB-7C37-4303-894C-A33A3951562B}.Release|x86.Build.0 = Release|x86 + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x64.ActiveCfg = Debug|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x64.Build.0 = Debug|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x86.ActiveCfg = Debug|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x86.Build.0 = Debug|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|Any CPU.Build.0 = Release|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x64.ActiveCfg = Release|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x64.Build.0 = Release|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x86.ActiveCfg = Release|Any CPU + {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection EndGlobal diff --git a/tools/DeltaIndexTestTool/Program.cs b/tools/DeltaIndexTestTool/Program.cs index 6e2f67a208..3e15a9a11e 100644 --- a/tools/DeltaIndexTestTool/Program.cs +++ b/tools/DeltaIndexTestTool/Program.cs @@ -8,6 +8,7 @@ namespace DeltaIndexTestTool using Microsoft.WinGetUtil.Interfaces; using System; using System.Collections.Generic; + using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -58,6 +59,9 @@ static int Main(string[] args) case "-h": PrintUsage(); return 0; + default: + PrintUsage(); + return 1; } } @@ -73,20 +77,16 @@ static int Main(string[] args) return 1; } - // Validate resume arguments: both must be provided together - bool hasResume = !string.IsNullOrEmpty(resumeCommit) || !string.IsNullOrEmpty(resumeWorkingIndexPath); - if (hasResume) + // --resume-working-index requires --resume-commit; the reverse is fine (build from scratch at that commit) + if (!string.IsNullOrEmpty(resumeWorkingIndexPath) && string.IsNullOrEmpty(resumeCommit)) { - if (string.IsNullOrEmpty(resumeCommit) || string.IsNullOrEmpty(resumeWorkingIndexPath)) - { - Console.Error.WriteLine("--resume-commit and --resume-working-index must be provided together."); - return 1; - } - if (!File.Exists(resumeWorkingIndexPath)) - { - Console.Error.WriteLine($"Resume working index not found: {resumeWorkingIndexPath}"); - return 1; - } + Console.Error.WriteLine("--resume-working-index requires --resume-commit."); + return 1; + } + if (!string.IsNullOrEmpty(resumeWorkingIndexPath) && !File.Exists(resumeWorkingIndexPath)) + { + Console.Error.WriteLine($"Resume working index not found: {resumeWorkingIndexPath}"); + return 1; } Directory.CreateDirectory(outputDir); @@ -118,12 +118,16 @@ static void PrintUsage() Console.WriteLine(" --max Maximum number of checkpoints; selects the N most"); Console.WriteLine(" recent intervals working backward from HEAD"); Console.WriteLine(" --branch Branch to walk (default: master)"); - Console.WriteLine(" --resume-commit Commit SHA to resume from (skip initial build)"); + Console.WriteLine(" --resume-commit Commit SHA to resume from"); Console.WriteLine(" --resume-working-index Path to pre-packaging working index for resume commit"); Console.WriteLine(); - Console.WriteLine("Resume: both --resume-* options must be provided together. The tool will"); - Console.WriteLine(" package the working index to produce checkpoint 0's full index, then"); - Console.WriteLine(" continue processing subsequent checkpoints from there."); + Console.WriteLine("Resume modes:"); + Console.WriteLine(" --resume-commit only Starts a fresh index at that commit, then continues"); + Console.WriteLine(" forward from there (skips re-walking older history)."); + Console.WriteLine(" --resume-commit + --resume-working-index"); + Console.WriteLine(" Uses the provided pre-built working index as checkpoint 0,"); + Console.WriteLine(" packages it, then continues with subsequent checkpoints."); + Console.WriteLine(" --resume-working-index requires --resume-commit."); Console.WriteLine(); Console.WriteLine("Output:"); Console.WriteLine(" results.csv CSV of checkpoint sizes"); @@ -137,11 +141,20 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in Console.WriteLine($"Output directory: {outputDir}"); Console.WriteLine($"Interval: every {intervalDays} day(s)"); - bool isResume = !string.IsNullOrEmpty(resumeCommit); + bool hasResumeCommit = !string.IsNullOrEmpty(resumeCommit); + bool hasResumeIndex = !string.IsNullOrEmpty(resumeWorkingIndexPath); + + // SelectCheckpoints uses the resume commit as an anchor: it only returns commits + // strictly after it. We always prepend the resume commit as checkpoints[0] so + // that the first ApplyGitDiff starts from the resume commit itself, ensuring no + // commits between the baseline and the first selected interval are skipped. + var checkpoints = SelectCheckpoints(repoPath, branch, intervalDays, maxCheckpoints, hasResumeCommit ? resumeCommit : null); - // When resuming, find the resume commit to use as anchor; checkpoints start after it. - // When --max is given without resume, work backward from HEAD to select the N most recent intervals. - var checkpoints = SelectCheckpoints(repoPath, branch, intervalDays, maxCheckpoints, isResume ? resumeCommit : null); + if (hasResumeCommit) + { + var resumeDate = LookupCommitDate(repoPath, resumeCommit); + checkpoints.Insert(0, new CommitCheckpoint(resumeCommit, resumeDate)); + } Console.WriteLine($"Selected {checkpoints.Count} checkpoints"); if (checkpoints.Count == 0) @@ -160,13 +173,13 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in for (int i = 0; i < checkpoints.Count; i++) { var checkpoint = checkpoints[i]; - Console.WriteLine($"\n[{i + 1}/{checkpoints.Count}] Processing checkpoint: {checkpoint.Commit.Sha[..8]} ({checkpoint.Date:yyyy-MM-dd})"); + Console.WriteLine($"\n[{i + 1}/{checkpoints.Count}] Processing checkpoint: {checkpoint.Sha[..8]} ({checkpoint.Date:yyyy-MM-dd})"); var result = new CheckpointResult { Index = i, Date = checkpoint.Date, - CommitSha = checkpoint.Commit.Sha[..8], + CommitSha = checkpoint.Sha[..8], }; string checkpointDir = Path.Combine(outputDir, $"checkpoint_{i:D4}"); @@ -175,23 +188,24 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in string fullIndexPath = Path.Combine(checkpointDir, "full_index.db"); string deltaPath = Path.Combine(checkpointDir, "delta.db"); - if (i == 0 && isResume) + if (i == 0 && hasResumeIndex) { - // Resume: copy the provided working index into position, then package it - // to produce this checkpoint's full index — same as a normal first checkpoint - // except we already have the working state. + // Resume with a pre-built working index: copy it into position and package. Console.WriteLine($" Resuming from provided working index (commit {resumeCommit[..Math.Min(8, resumeCommit.Length)]})"); File.Copy(resumeWorkingIndexPath, workingIndexPath, overwrite: true); + string savedWorkingPath = Path.Combine(checkpointDir, "working_index.db"); + File.Copy(workingIndexPath, savedWorkingPath, overwrite: true); + File.Copy(workingIndexPath, fullIndexPath, overwrite: true); using (var packagingIndex = factory.SQLiteIndexOpen(fullIndexPath)) { + packagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); packagingIndex.PrepareForPackaging(); } workingIndex = factory.SQLiteIndexOpen(workingIndexPath); - workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); result.FullIndexBytes = new FileInfo(fullIndexPath).Length; result.DeltaBytes = 0; @@ -202,7 +216,7 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in } else if (i == 0) { - // First checkpoint: build from scratch + // First checkpoint with no pre-built index: build from scratch at the resume commit. Console.WriteLine(" Building initial full index from scratch..."); if (File.Exists(workingIndexPath)) File.Delete(workingIndexPath); @@ -210,20 +224,23 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in workingIndex = factory.SQLiteIndexCreate(workingIndexPath, 2u, 1u); workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, "0"); - int added = AddAllManifests(workingIndex, repoPath, checkpoint.Commit, checkpointDir); + int added = AddAllManifests(workingIndex, repoPath, checkpoint.Sha); Console.WriteLine($" Added {added} manifest files"); workingIndex.Dispose(); workingIndex = null; + string savedWorkingPath = Path.Combine(checkpointDir, "working_index.db"); + File.Copy(workingIndexPath, savedWorkingPath, overwrite: true); + File.Copy(workingIndexPath, fullIndexPath, overwrite: true); using (var packagingIndex = factory.SQLiteIndexOpen(fullIndexPath)) { + packagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); packagingIndex.PrepareForPackaging(); } workingIndex = factory.SQLiteIndexOpen(workingIndexPath); - workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); result.FullIndexBytes = new FileInfo(fullIndexPath).Length; result.DeltaBytes = 0; @@ -239,17 +256,21 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in string prevFullIndexPath = results[i - 1].FullIndexPath!; Console.WriteLine(" Applying git diff from previous checkpoint..."); - int changed = ApplyGitDiff(workingIndex!, repoPath, prevCheckpoint.Commit, checkpoint.Commit, checkpointDir); + int changed = ApplyGitDiff(workingIndex!, repoPath, prevCheckpoint.Sha, checkpoint.Sha, checkpointDir); Console.WriteLine($" Applied {changed} manifest changes"); workingIndex!.Dispose(); workingIndex = null; + string savedWorkingPath = Path.Combine(checkpointDir, "working_index.db"); + File.Copy(workingIndexPath, savedWorkingPath, overwrite: true); + // Build full index (no delta properties set) string fullOnlyPath = fullIndexPath + ".full_only.db"; File.Copy(workingIndexPath, fullOnlyPath, overwrite: true); using (var fullPackagingIndex = factory.SQLiteIndexOpen(fullOnlyPath)) { + fullPackagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); fullPackagingIndex.PrepareForPackaging(); } File.Move(fullOnlyPath, fullIndexPath, overwrite: true); @@ -261,12 +282,12 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in { deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaBaselineIndexPath, Path.GetFullPath(prevFullIndexPath)); deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaOutputPath, Path.GetFullPath(deltaPath)); + deltaPackagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); deltaPackagingIndex.PrepareForPackaging(); } File.Delete(deltaWorkPath); workingIndex = factory.SQLiteIndexOpen(workingIndexPath); - workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); result.FullIndexBytes = new FileInfo(fullIndexPath).Length; result.DeltaBytes = File.Exists(deltaPath) ? new FileInfo(deltaPath).Length : 0; @@ -296,6 +317,14 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in Console.WriteLine($"Report written to: {htmlPath}"); } + static DateTime LookupCommitDate(string repoPath, string sha) + { + using var repo = new Repository(repoPath); + var commit = repo.Lookup(sha) + ?? throw new InvalidOperationException($"Commit '{sha}' not found in repository"); + return commit.Author.When.DateTime; + } + /// /// Selects commits at evenly-spaced intervals across the branch history. /// @@ -315,22 +344,12 @@ static List SelectCheckpoints(string repoPath, string branch, throw new InvalidOperationException($"Branch '{branch}' not found in repository"); } - // Collect all commits sorted newest-first - var allCommits = repo.Commits - .QueryBy(new CommitFilter - { - IncludeReachableFrom = branchRef.Tip, - SortBy = CommitSortStrategies.Time, - }) - .ToList(); - - if (allCommits.Count == 0) return []; - - // If resuming, find the anchor commit and exclude it and anything older + // Resolve the anchor commit's timestamp without loading all commits. + // repo.Lookup handles full and abbreviated SHAs efficiently. DateTimeOffset? afterTime = null; if (!string.IsNullOrEmpty(afterCommitSha)) { - var anchor = allCommits.FirstOrDefault(c => c.Sha.StartsWith(afterCommitSha, StringComparison.OrdinalIgnoreCase)); + var anchor = repo.Lookup(afterCommitSha); if (anchor == null) { throw new InvalidOperationException($"Resume commit '{afterCommitSha}' not found on branch '{branch}'"); @@ -338,179 +357,302 @@ static List SelectCheckpoints(string repoPath, string branch, afterTime = anchor.Author.When; } - if (maxCheckpoints > 0) + var filter = new CommitFilter { - // Work backward from HEAD: pick intervals going back in time, then reverse - var selected = new List(); - DateTimeOffset? lastSelected = null; + IncludeReachableFrom = branchRef.Tip, + SortBy = CommitSortStrategies.Time, + }; + + // Stream commits newest-first; store only SHA strings to avoid holding native + // libgit2 handles beyond the Repository lifetime. Both the --max and no-max + // paths work backward from HEAD and then reverse, which avoids a full + // materialization of the 300K+ commit walk. + var selected = new List(); + DateTimeOffset? lastSelected = null; + + foreach (var commit in repo.Commits.QueryBy(filter)) // newest-first, lazy + { + var commitTime = commit.Author.When; + + // Stop as soon as we pass the resume anchor + if (afterTime.HasValue && commitTime <= afterTime.Value) + break; - foreach (var commit in allCommits) // newest-first + if (lastSelected == null || (lastSelected.Value - commitTime).TotalDays >= intervalDays) { - var commitTime = commit.Author.When; + selected.Add(new CommitCheckpoint(commit.Sha, commitTime.DateTime)); + lastSelected = commitTime; - // Skip commits at or before the resume anchor - if (afterTime.HasValue && commitTime <= afterTime.Value) + if (maxCheckpoints > 0 && selected.Count >= maxCheckpoints) break; + } + } - if (lastSelected == null || (lastSelected.Value - commitTime).TotalDays >= intervalDays) - { - selected.Add(new CommitCheckpoint(commit, commitTime.DateTime)); - lastSelected = commitTime; + // Return in chronological order (oldest first) + selected.Reverse(); + return selected; + } - if (selected.Count >= maxCheckpoints) - break; - } - } + /// + /// Checks out the target commit in the repository, then walks the filesystem to collect + /// manifest directories and add them to the index. The repository is left at the target + /// commit on return (no state is restored). + /// Retries failures until no further progress can be made (resolves dependency ordering). + /// Returns the count of manifests successfully added. + /// + static int AddAllManifests(IWinGetSQLiteIndex index, string repoPath, string commitSha) + { + Console.WriteLine($" Checking out commit {commitSha[..8]}..."); + RunGit(repoPath, $"checkout --detach {commitSha}"); + + string manifestsRoot = Path.Combine(repoPath, "manifests"); + if (!Directory.Exists(manifestsRoot)) return 0; + + // Collect manifest version directories: any directory that contains .yaml files directly. + Console.WriteLine(" Collecting manifest directories from filesystem..."); + var manifests = Directory + .EnumerateFiles(manifestsRoot, "*.yaml", SearchOption.AllDirectories) + .GroupBy(f => Path.GetDirectoryName(f)!, StringComparer.OrdinalIgnoreCase) + .Select(g => ( + LocalDir: g.Key, + RelPath: Path.GetRelativePath(repoPath, g.Key).Replace(Path.DirectorySeparatorChar, '/'))) + .ToList(); + Console.WriteLine($" Found {manifests.Count} manifest directories"); - // Return in chronological order (oldest first) - selected.Reverse(); - return selected; - } - else + // Initial add pass — collect failures, printing periodic progress. + var failed = new List<(string LocalDir, string RelPath)>(); + int total = manifests.Count; + int done = 0; + foreach (var (localDir, relPath) in manifests) { - // Walk oldest-first, selecting at each interval - var chronological = allCommits - .Where(c => !afterTime.HasValue || c.Author.When > afterTime.Value) - .Reverse() - .ToList(); + try { index.AddManifest(localDir, relPath); } + catch { failed.Add((localDir, relPath)); } - var selected = new List(); - DateTimeOffset? lastSelected = null; + done++; + if (done % 500 == 0 || done == total) + Console.Write($"\r Adding: {done}/{total} ({100.0 * done / total:F1}%) "); + } + Console.WriteLine(); // end the \r line - foreach (var commit in chronological) + // Retry loop: keep going as long as at least one failure is resolved each round. + int pass = 1; + while (failed.Count > 0) + { + var retrying = failed; + failed = []; + Console.WriteLine($" Retry pass {pass}: {retrying.Count} manifest(s) pending..."); + foreach (var (localDir, relPath) in retrying) { - var commitTime = commit.Author.When; - - if (lastSelected == null || (commitTime - lastSelected.Value).TotalDays >= intervalDays) - { - selected.Add(new CommitCheckpoint(commit, commitTime.DateTime)); - lastSelected = commitTime; - } + try { index.AddManifest(localDir, relPath); } + catch { failed.Add((localDir, relPath)); } } - return selected; + // No progress this round — stop. + if (failed.Count == retrying.Count) break; + pass++; + } + + foreach (var (_, relPath) in failed) + { + Console.Error.WriteLine($" Could not add manifest (no progress): {relPath}"); + } + + return manifests.Count - failed.Count; + } + + static void RunGit(string repoPath, string arguments) + { + var psi = new ProcessStartInfo("git") + { + Arguments = $"-C \"{repoPath}\" {arguments}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + using var p = Process.Start(psi)!; + p.WaitForExit(); + if (p.ExitCode != 0) + { + string err = p.StandardError.ReadToEnd().Trim(); + throw new InvalidOperationException($"git {arguments} failed (exit {p.ExitCode}): {err}"); + } + } + + static IEnumerable RunGitLines(string repoPath, string arguments) + { + var psi = new ProcessStartInfo("git") + { + Arguments = $"-C \"{repoPath}\" {arguments}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + using var p = Process.Start(psi)!; + string? line; + while ((line = p.StandardOutput.ReadLine()) != null) + { + if (!string.IsNullOrEmpty(line)) + yield return line; + } + p.WaitForExit(); + if (p.ExitCode != 0) + { + string err = p.StandardError.ReadToEnd().Trim(); + throw new InvalidOperationException($"git {arguments} failed (exit {p.ExitCode}): {err}"); } } /// - /// Extracts all YAML manifest files from a git commit to a temp directory and adds them to the index. - /// Returns the count of manifests added. + /// Walks every commit between (exclusive) and + /// (inclusive) in topological order (oldest first) using + /// git rev-list --topo-order --reverse, then applies each commit's manifest + /// change to the index. Each commit is expected to touch at most one manifest directory. + /// Returns the number of index operations that succeeded. /// - static int AddAllManifests(IWinGetSQLiteIndex index, string repoPath, Commit commit, string workDir) + static int ApplyGitDiff(IWinGetSQLiteIndex index, string repoPath, string fromSha, string toSha, string workDir) { - string tempDir = Path.Combine(workDir, "manifests_full"); + string tempDir = Path.Combine(workDir, "manifests_diff"); if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); Directory.CreateDirectory(tempDir); - using var repo = new Repository(repoPath); - var manifestsDir = commit.Tree["manifests"]; - if (manifestsDir == null) return 0; + // Use git rev-list to obtain the definitive topological ordering (oldest-first). + // This matches exactly what `git log --topo-order --reverse` produces and avoids + // any ambiguity in LibGit2Sharp's CommitSortStrategies.Topological | Reverse. + var commitShas = RunGitLines(repoPath, $"rev-list --topo-order --reverse {fromSha}..{toSha}") + .ToList(); - int count = ExtractAndAddTree(index, repo, (Tree)manifestsDir.Target, tempDir, "manifests"); - return count; - } + if (commitShas.Count == 0) return 0; - static int ExtractAndAddTree(IWinGetSQLiteIndex index, Repository repo, Tree tree, string baseDir, string relativePath) - { int count = 0; - foreach (var entry in tree) + + string logPath = Path.Combine(workDir, "manifest_operations.txt"); + using var log = new StreamWriter(logPath, append: false, Encoding.UTF8); + log.WriteLine("Commit\tTimestamp\tOperation\tPath\tError"); + + using var repo = new Repository(repoPath); + + foreach (var sha in commitShas) { - string entryRelPath = relativePath + "/" + entry.Name; - if (entry.TargetType == TreeEntryTargetType.Tree) + var commit = repo.Lookup(sha); + var parent = commit.Parents.FirstOrDefault(); + var diff = repo.Diff.Compare(parent?.Tree, commit.Tree); + + // Collect per-directory changes within this commit. + // A "move" commit deletes one version dir and adds another — both must be processed. + // Key: directory path. Value: (anyAdded, anyDeleted, anyModified). + var dirChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var change in diff) { - count += ExtractAndAddTree(index, repo, (Tree)entry.Target, baseDir, entryRelPath); + string entryPath = change.Status == ChangeKind.Deleted ? change.OldPath : change.Path; + + if (!entryPath.StartsWith("manifests/", StringComparison.OrdinalIgnoreCase) || + !entryPath.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + int lastSlash = entryPath.LastIndexOf('/'); + string dir = lastSlash > 0 ? entryPath[..lastSlash] : string.Empty; + + dirChanges.TryGetValue(dir, out var flags); + dirChanges[dir] = change.Status switch + { + ChangeKind.Added => (true, flags.AnyDeleted, flags.AnyModified), + ChangeKind.Deleted => (flags.AnyAdded, true, flags.AnyModified), + _ => (flags.AnyAdded, flags.AnyDeleted, true), + }; } - else if (entry.TargetType == TreeEntryTargetType.Blob && entry.Name.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) + + if (dirChanges.Count == 0) continue; // No manifest changes in this commit + + // Pure deletes first, then updates (remove+add), then pure adds. + // This ensures move commits (delete old dir, add new dir) remove before adding. + static int OpOrder((bool AnyAdded, bool AnyDeleted, bool AnyModified) f) => + (!f.AnyAdded && !f.AnyModified) ? 0 : // pure delete + (!f.AnyDeleted && !f.AnyModified) ? 2 : // pure add + 1; // update (remove+add) + + foreach (var (dirPath, (anyAdded, anyDeleted, anyModified)) in dirChanges.OrderBy(kv => OpOrder(kv.Value))) { - var blob = (Blob)entry.Target; - string localPath = Path.Combine(baseDir, entryRelPath.Replace('/', Path.DirectorySeparatorChar)); - Directory.CreateDirectory(Path.GetDirectoryName(localPath)!); - File.WriteAllBytes(localPath, blob.GetContentStream().ReadAllBytes()); + bool isPureDelete = !anyAdded && !anyModified; + bool isPureAdd = !anyDeleted && !anyModified; + // Everything else is an update: remove the old state then add the new state. - try + if (isPureDelete) { - index.AddManifest(localPath, entryRelPath); - count++; + string localDir = ExtractManifestDirFromTree(repo, parent!, dirPath, tempDir); + TryIndexOp(index, log, commit, "remove", dirPath, localDir, + (idx, dir, path) => idx.RemoveManifest(dir, path), ref count); } - catch + else if (isPureAdd) { - // Dependency ordering issues — skip for now (same behavior as IndexCreationTool) + string localDir = ExtractManifestDirFromTree(repo, commit, dirPath, tempDir); + TryIndexOp(index, log, commit, "add", dirPath, localDir, + (idx, dir, path) => idx.AddManifest(dir, path), ref count); + } + else + { + // Update: remove using pre-commit state, then add using post-commit state. + string removeDir = ExtractManifestDirFromTree(repo, parent!, dirPath, tempDir); + TryIndexOp(index, log, commit, "remove", dirPath, removeDir, + (idx, dir, path) => idx.RemoveManifest(dir, path), ref count); + + string addDir = ExtractManifestDirFromTree(repo, commit, dirPath, tempDir); + TryIndexOp(index, log, commit, "add", dirPath, addDir, + (idx, dir, path) => idx.AddManifest(dir, path), ref count); } } } + return count; } + static void TryIndexOp(IWinGetSQLiteIndex index, StreamWriter log, Commit commit, + string operationName, string dirPath, string localDir, + Action op, ref int count) + { + try + { + op(index, localDir, dirPath); + log.WriteLine($"{commit.Sha}\t{commit.Author.When:yyyy-MM-dd HH:mm:ss zzz}\t{operationName}\t{dirPath}\t"); + count++; + } + catch (Exception ex) + { + log.WriteLine($"{commit.Sha}\t{commit.Author.When:yyyy-MM-dd HH:mm:ss zzz}\t{operationName}\t{dirPath}\t{ex.HResult}"); + Console.Error.WriteLine($" Failed to {operationName} manifest '{dirPath}': {ex.HResult}"); + } + } + /// - /// Applies the git diff between two commits to the working index. - /// Returns the total number of changed manifest entries. + /// Extracts all YAML files from the given in the commit's tree + /// into a subdirectory of that includes the first 8 characters + /// of the commit SHA, ensuring no cross-commit directory conflicts. + /// Returns the local directory path used. /// - static int ApplyGitDiff(IWinGetSQLiteIndex index, string repoPath, Commit fromCommit, Commit toCommit, string workDir) + static string ExtractManifestDirFromTree(Repository repo, Commit commit, string dirPath, string tempDir) { - string tempDir = Path.Combine(workDir, "manifests_diff"); - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); - Directory.CreateDirectory(tempDir); - - using var repo = new Repository(repoPath); + string localDir = Path.Combine( + tempDir, + dirPath.Replace('/', Path.DirectorySeparatorChar), + commit.Sha[..8]); - var diff = repo.Diff.Compare(fromCommit.Tree, toCommit.Tree); - int count = 0; + var entry = commit[dirPath]; + if (entry?.TargetType != TreeEntryTargetType.Tree) return localDir; - foreach (var change in diff) + Directory.CreateDirectory(localDir); + foreach (var child in (Tree)entry.Target) { - // Only process YAML files under manifests/ - if (!change.Path.StartsWith("manifests/", StringComparison.OrdinalIgnoreCase) || - !change.Path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) + if (child.TargetType == TreeEntryTargetType.Blob && + child.Name.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) { - continue; - } - - try - { - switch (change.Status) - { - case ChangeKind.Added: - { - string localPath = ExtractBlobToTemp(repo, toCommit, change.Path, tempDir); - index.AddManifest(localPath, change.Path); - count++; - break; - } - case ChangeKind.Modified: - case ChangeKind.Renamed: - { - string localPath = ExtractBlobToTemp(repo, toCommit, change.Path, tempDir); - index.UpdateManifest(localPath, change.Path); - count++; - break; - } - case ChangeKind.Deleted: - { - // For removal we need the old content to get the package ID - string localPath = ExtractBlobToTemp(repo, fromCommit, change.OldPath, tempDir); - index.RemoveManifest(localPath, change.OldPath); - count++; - break; - } - } - } - catch - { - // Skip manifest processing errors (e.g., dependency issues) + var blob = (Blob)child.Target; + File.WriteAllBytes(Path.Combine(localDir, child.Name), blob.GetContentStream().ReadAllBytes()); } } - return count; - } - - static string ExtractBlobToTemp(Repository repo, Commit commit, string path, string tempDir) - { - var entry = commit[path]; - var blob = (Blob)entry.Target; - string localPath = Path.Combine(tempDir, path.Replace('/', Path.DirectorySeparatorChar)); - Directory.CreateDirectory(Path.GetDirectoryName(localPath)!); - File.WriteAllBytes(localPath, blob.GetContentStream().ReadAllBytes()); - return localPath; + return localDir; } static void ComputeCumulativeSizes(List results) @@ -607,7 +749,7 @@ static void WriteHtmlReport(List results, string path) } } - record CommitCheckpoint(Commit Commit, DateTime Date); + record CommitCheckpoint(string Sha, DateTime Date); class CheckpointResult { From 47f138e3086df840987aa2e03c406e6392f18f74 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 16 Mar 2026 17:41:02 -0700 Subject: [PATCH 04/36] maybe updates work? --- tools/DeltaIndexTestTool/Program.cs | 45 ++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/tools/DeltaIndexTestTool/Program.cs b/tools/DeltaIndexTestTool/Program.cs index 3e15a9a11e..65377c7013 100644 --- a/tools/DeltaIndexTestTool/Program.cs +++ b/tools/DeltaIndexTestTool/Program.cs @@ -544,24 +544,41 @@ static int ApplyGitDiff(IWinGetSQLiteIndex index, string repoPath, string fromSh foreach (var change in diff) { - string entryPath = change.Status == ChangeKind.Deleted ? change.OldPath : change.Path; - - if (!entryPath.StartsWith("manifests/", StringComparison.OrdinalIgnoreCase) || - !entryPath.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) + // For renames, the old and new paths can be in different directories (e.g., a + // version bump moves manifests from /1.0.0/ to /2.0.0/). We must record the + // deletion against the OLD directory and the addition against the NEW directory + // independently; collapsing both sides to a single path would cause the update + // path to look up the wrong tree when extracting the pre-commit state. + + // Old side: record deletion in the source directory. + if (change.Status == ChangeKind.Deleted || change.Status == ChangeKind.Renamed) { - continue; + string oldPath = change.OldPath; + if (oldPath.StartsWith("manifests/", StringComparison.OrdinalIgnoreCase) && + oldPath.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) + { + int lastSlash = oldPath.LastIndexOf('/'); + string dir = lastSlash > 0 ? oldPath[..lastSlash] : string.Empty; + dirChanges.TryGetValue(dir, out var flags); + dirChanges[dir] = (flags.AnyAdded, true, flags.AnyModified); + } } - int lastSlash = entryPath.LastIndexOf('/'); - string dir = lastSlash > 0 ? entryPath[..lastSlash] : string.Empty; - - dirChanges.TryGetValue(dir, out var flags); - dirChanges[dir] = change.Status switch + // New side: record addition/modification in the destination directory. + if (change.Status != ChangeKind.Deleted) { - ChangeKind.Added => (true, flags.AnyDeleted, flags.AnyModified), - ChangeKind.Deleted => (flags.AnyAdded, true, flags.AnyModified), - _ => (flags.AnyAdded, flags.AnyDeleted, true), - }; + string newPath = change.Path; + if (newPath.StartsWith("manifests/", StringComparison.OrdinalIgnoreCase) && + newPath.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) + { + int lastSlash = newPath.LastIndexOf('/'); + string dir = lastSlash > 0 ? newPath[..lastSlash] : string.Empty; + dirChanges.TryGetValue(dir, out var flags); + dirChanges[dir] = (change.Status == ChangeKind.Added || change.Status == ChangeKind.Renamed) + ? (true, flags.AnyDeleted, flags.AnyModified) + : (flags.AnyAdded, flags.AnyDeleted, true); + } + } } if (dirChanges.Count == 0) continue; // No manifest changes in this commit From 7d10994651b3794de4d4f94678939096406129d0 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 17 Mar 2026 19:43:50 -0700 Subject: [PATCH 05/36] Delta against both prev and orig db, no html --- tools/DeltaIndexTestTool/Program.cs | 131 +++++++--------------------- 1 file changed, 30 insertions(+), 101 deletions(-) diff --git a/tools/DeltaIndexTestTool/Program.cs b/tools/DeltaIndexTestTool/Program.cs index 3e15a9a11e..2ccfa54a58 100644 --- a/tools/DeltaIndexTestTool/Program.cs +++ b/tools/DeltaIndexTestTool/Program.cs @@ -186,7 +186,8 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in Directory.CreateDirectory(checkpointDir); string fullIndexPath = Path.Combine(checkpointDir, "full_index.db"); - string deltaPath = Path.Combine(checkpointDir, "delta.db"); + string deltaPrevPath = Path.Combine(checkpointDir, "delta_prev.db"); + string deltaOrigPath = Path.Combine(checkpointDir, "delta_orig.db"); if (i == 0 && hasResumeIndex) { @@ -208,7 +209,6 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in workingIndex = factory.SQLiteIndexOpen(workingIndexPath); result.FullIndexBytes = new FileInfo(fullIndexPath).Length; - result.DeltaBytes = 0; result.PreviousFullIndexPath = null; result.FullIndexPath = fullIndexPath; @@ -243,7 +243,6 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in workingIndex = factory.SQLiteIndexOpen(workingIndexPath); result.FullIndexBytes = new FileInfo(fullIndexPath).Length; - result.DeltaBytes = 0; result.PreviousFullIndexPath = null; result.FullIndexPath = fullIndexPath; @@ -254,6 +253,7 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in // Subsequent checkpoint: apply git diff to working index var prevCheckpoint = checkpoints[i - 1]; string prevFullIndexPath = results[i - 1].FullIndexPath!; + string origFullIndexPath = results[0].FullIndexPath!; Console.WriteLine(" Applying git diff from previous checkpoint..."); int changed = ApplyGitDiff(workingIndex!, repoPath, prevCheckpoint.Sha, checkpoint.Sha, checkpointDir); @@ -276,26 +276,40 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in File.Move(fullOnlyPath, fullIndexPath, overwrite: true); // Build delta index against previous full index - string deltaWorkPath = fullIndexPath + ".delta_work.db"; - File.Copy(workingIndexPath, deltaWorkPath, overwrite: true); - using (var deltaPackagingIndex = factory.SQLiteIndexOpen(deltaWorkPath)) + string deltaPrevWorkPath = fullIndexPath + ".delta_prev_cp.db"; + File.Copy(workingIndexPath, deltaPrevWorkPath, overwrite: true); + using (var deltaPackagingIndex = factory.SQLiteIndexOpen(deltaPrevWorkPath)) { deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaBaselineIndexPath, Path.GetFullPath(prevFullIndexPath)); - deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaOutputPath, Path.GetFullPath(deltaPath)); + deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaOutputPath, Path.GetFullPath(deltaPrevPath)); deltaPackagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); deltaPackagingIndex.PrepareForPackaging(); } - File.Delete(deltaWorkPath); + File.Delete(deltaPrevWorkPath); + + // Build delta index against previous full index + string deltaOrigWorkPath = fullIndexPath + ".delta_orig_cp.db"; + File.Copy(workingIndexPath, deltaOrigWorkPath, overwrite: true); + using (var deltaPackagingIndex = factory.SQLiteIndexOpen(deltaOrigWorkPath)) + { + deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaBaselineIndexPath, Path.GetFullPath(origFullIndexPath)); + deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaOutputPath, Path.GetFullPath(deltaOrigPath)); + deltaPackagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); + deltaPackagingIndex.PrepareForPackaging(); + } + File.Delete(deltaOrigWorkPath); workingIndex = factory.SQLiteIndexOpen(workingIndexPath); result.FullIndexBytes = new FileInfo(fullIndexPath).Length; - result.DeltaBytes = File.Exists(deltaPath) ? new FileInfo(deltaPath).Length : 0; + result.DeltaPrevBytes = File.Exists(deltaPrevPath) ? new FileInfo(deltaPrevPath).Length : 0; + result.DeltaOrigBytes = File.Exists(deltaOrigPath) ? new FileInfo(deltaOrigPath).Length : 0; result.PreviousFullIndexPath = prevFullIndexPath; result.FullIndexPath = fullIndexPath; Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB"); - Console.WriteLine($" Delta: {result.DeltaBytes / 1024.0 / 1024.0:F2} MB"); + Console.WriteLine($" Delta prev: {result.DeltaPrevBytes / 1024.0 / 1024.0:F2} MB"); + Console.WriteLine($" Delta orig: {result.DeltaOrigBytes / 1024.0 / 1024.0:F2} MB"); } results.Add(result); @@ -306,15 +320,9 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in workingIndex?.Dispose(); } - ComputeCumulativeSizes(results); - string csvPath = Path.Combine(outputDir, "results.csv"); WriteCsv(results, csvPath); Console.WriteLine($"\nResults written to: {csvPath}"); - - string htmlPath = Path.Combine(outputDir, "report.html"); - WriteHtmlReport(results, htmlPath); - Console.WriteLine($"Report written to: {htmlPath}"); } static DateTime LookupCommitDate(string repoPath, string sha) @@ -655,97 +663,19 @@ static string ExtractManifestDirFromTree(Repository repo, Commit commit, string return localDir; } - static void ComputeCumulativeSizes(List results) - { - long cumulativeFull = 0; - long cumulativeDelta = 0; - - for (int i = 0; i < results.Count; i++) - { - cumulativeFull += results[i].FullIndexBytes; - results[i].CumulativeFullDownloadBytes = cumulativeFull; - - if (i == 0) - { - cumulativeDelta += results[i].FullIndexBytes; // First checkpoint: must download full - } - else - { - cumulativeDelta += results[i].DeltaBytes; // Subsequent: download delta only - } - results[i].CumulativeDeltaDownloadBytes = cumulativeDelta; - } - } - static void WriteCsv(List results, string path) { using var writer = new StreamWriter(path, false, Encoding.UTF8); - writer.WriteLine("Index,Date,CommitSha,FullIndexMB,DeltaMB,CumulativeFullMB,CumulativeDeltaMB,SavingsPercent"); + writer.WriteLine("Index,Date,CommitSha,FullIndexMB,DeltaPrevMB,DeltaOrigMB"); foreach (var r in results) { double fullMb = r.FullIndexBytes / 1024.0 / 1024.0; - double deltaMb = r.DeltaBytes / 1024.0 / 1024.0; - double cumFullMb = r.CumulativeFullDownloadBytes / 1024.0 / 1024.0; - double cumDeltaMb = r.CumulativeDeltaDownloadBytes / 1024.0 / 1024.0; - double savings = r.CumulativeFullDownloadBytes > 0 - ? 100.0 * (1.0 - (double)r.CumulativeDeltaDownloadBytes / r.CumulativeFullDownloadBytes) - : 0; - - writer.WriteLine($"{r.Index},{r.Date:yyyy-MM-dd},{r.CommitSha},{fullMb:F2},{deltaMb:F2},{cumFullMb:F2},{cumDeltaMb:F2},{savings:F1}"); - } - } - - static void WriteHtmlReport(List results, string path) - { - var sb = new StringBuilder(); - sb.AppendLine(""); - sb.AppendLine("Delta Index Size Analysis"); - sb.AppendLine(""); - sb.AppendLine(""); - sb.AppendLine(""); - sb.AppendLine("

Delta Index Size Analysis

"); - sb.AppendLine("

Cumulative Download: Full Strategy vs Delta Strategy

"); - sb.AppendLine(""); - sb.AppendLine(""); - - // Summary table - sb.AppendLine("

Per-Checkpoint Details

"); - sb.AppendLine(""); + double deltaPrevMb = r.DeltaPrevBytes / 1024.0 / 1024.0; + double deltaOrigMb = r.DeltaOrigBytes / 1024.0 / 1024.0; - foreach (var r in results) - { - double savings = r.CumulativeFullDownloadBytes > 0 - ? 100.0 * (1.0 - (double)r.CumulativeDeltaDownloadBytes / r.CumulativeFullDownloadBytes) - : 0; - - sb.AppendLine($"" + - $"" + - $"" + - $"" + - $"" + - $""); + writer.WriteLine($"{r.Index},{r.Date:yyyy-MM-dd},{r.CommitSha},{fullMb:F2},{deltaPrevMb:F2},{deltaOrigMb:F2}"); } - - sb.AppendLine("
IndexDateCommitFull Index (MB)Delta (MB)Cum. Full (MB)Cum. Delta (MB)Savings (%)
{r.Index}{r.Date:yyyy-MM-dd}{r.CommitSha}{r.FullIndexBytes / 1024.0 / 1024.0:F2}{r.DeltaBytes / 1024.0 / 1024.0:F2}{r.CumulativeFullDownloadBytes / 1024.0 / 1024.0:F2}{r.CumulativeDeltaDownloadBytes / 1024.0 / 1024.0:F2}{savings:F1}%
"); - sb.AppendLine(""); - File.WriteAllText(path, sb.ToString(), Encoding.UTF8); } } @@ -757,9 +687,8 @@ class CheckpointResult public DateTime Date { get; set; } public string CommitSha { get; set; } = string.Empty; public long FullIndexBytes { get; set; } - public long DeltaBytes { get; set; } - public long CumulativeFullDownloadBytes { get; set; } - public long CumulativeDeltaDownloadBytes { get; set; } + public long DeltaPrevBytes { get; set; } + public long DeltaOrigBytes { get; set; } public string? FullIndexPath { get; set; } public string? PreviousFullIndexPath { get; set; } } From fb18fcb476a081849ae8368b199c31e0bfec61dd Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 18 Mar 2026 17:27:32 -0700 Subject: [PATCH 06/36] Handle add-only updates and enable stateful resumes --- .../Microsoft/Schema/2_0/Interface_2_0.cpp | 170 +++++++------ tools/DeltaIndexTestTool/Program.cs | 225 ++++++++++++++++-- 2 files changed, 282 insertions(+), 113 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index c345a32c5a..50f10d2f38 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -1073,108 +1073,102 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } auto changedPackages = PackageUpdateTrackingTable::GetUpdatesSince(connection, deltaUpdateBaseTime); - if (changedPackages.empty()) - { - AICLI_LOG(Repo, Info, << "No changed packages found; skipping delta generation"); - } - else - { - SQLite::Connection deltaConn = SQLite::Connection::Create( - deltaOutputPath.u8string(), SQLite::Connection::OpenDisposition::Create); - anon::CreateDeltaSchema(deltaConn); + SQLite::Connection deltaConn = SQLite::Connection::Create( + deltaOutputPath.u8string(), SQLite::Connection::OpenDisposition::Create); + + anon::CreateDeltaSchema(deltaConn); - SQLite::rowid_t maxBaselinePackageRowid = anon::GetMaxPackageRowid(baselineConn); - SQLite::rowid_t nextNewPackageRowid = maxBaselinePackageRowid; + SQLite::rowid_t maxBaselinePackageRowid = anon::GetMaxPackageRowid(baselineConn); + SQLite::rowid_t nextNewPackageRowid = maxBaselinePackageRowid; - SQLite::rowid_t maxBaselineTagsRowid = anon::GetMaxDataTableRowid(baselineConn, "tags2"); - SQLite::rowid_t nextNewTagsRowid = maxBaselineTagsRowid; + SQLite::rowid_t maxBaselineTagsRowid = anon::GetMaxDataTableRowid(baselineConn, "tags2"); + SQLite::rowid_t nextNewTagsRowid = maxBaselineTagsRowid; - SQLite::rowid_t maxBaselineCommandsRowid = anon::GetMaxDataTableRowid(baselineConn, "commands2"); - SQLite::rowid_t nextNewCommandsRowid = maxBaselineCommandsRowid; + SQLite::rowid_t maxBaselineCommandsRowid = anon::GetMaxDataTableRowid(baselineConn, "commands2"); + SQLite::rowid_t nextNewCommandsRowid = maxBaselineCommandsRowid; - SQLite::Savepoint deltaSavepoint = SQLite::Savepoint::Create(deltaConn, "delta_build"); + SQLite::Savepoint deltaSavepoint = SQLite::Savepoint::Create(deltaConn, "delta_build"); - for (const auto& pkg : changedPackages) + for (const auto& pkg : changedPackages) + { + SQLite::rowid_t packageRowid = anon::GetBaselinePackageRowid(baselineConn, pkg.PackageIdentifier); + + if (pkg.IsRemoved) { - SQLite::rowid_t packageRowid = anon::GetBaselinePackageRowid(baselineConn, pkg.PackageIdentifier); + if (packageRowid == 0) + { + // Package was added and removed within the same tracking window; skip. + continue; + } + + AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); - if (pkg.IsRemoved) + std::string sql = "INSERT OR REPLACE INTO delta_packages (rowid, id, name, latest_version, is_removed) VALUES (?, ?, '', '', 1)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, packageRowid); + stmt.Bind(2, pkg.PackageIdentifier); + stmt.Execute(); + } + else + { + bool isNewPackage = (packageRowid == 0); + if (isNewPackage) { - if (packageRowid == 0) - { - // Package was added and removed within the same tracking window; skip. - continue; - } + packageRowid = ++nextNewPackageRowid; + } - AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); + AICLI_LOG(Repo, Verbose, << "Delta: recording " << (isNewPackage ? "addition" : "update") << " of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); - std::string sql = "INSERT OR REPLACE INTO delta_packages (rowid, id, name, latest_version, is_removed) VALUES (?, ?, '', '', 1)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, packageRowid); - stmt.Bind(2, pkg.PackageIdentifier); - stmt.Execute(); + { + std::string sql = "SELECT id, name, moniker, latest_version, arp_min_version, arp_max_version, hash " + "FROM packages WHERE id LIKE ?"; + SQLite::Statement stmt = SQLite::Statement::Create(connection, sql); + stmt.Bind(1, pkg.PackageIdentifier); + THROW_HR_IF(E_NOT_SET, !stmt.Step()); + + std::string id = stmt.GetColumn(0); + std::string name = stmt.GetColumn(1); + std::string moniker = stmt.GetColumnIsNull(2) ? "" : stmt.GetColumn(2); + std::string latestVersion = stmt.GetColumn(3); + std::string arpMin = stmt.GetColumnIsNull(4) ? "" : stmt.GetColumn(4); + std::string arpMax = stmt.GetColumnIsNull(5) ? "" : stmt.GetColumn(5); + SQLite::blob_t hash = stmt.GetColumnIsNull(6) ? SQLite::blob_t{} : stmt.GetColumn(6); + + std::string insertSql = + "INSERT OR REPLACE INTO delta_packages " + "(rowid, id, name, moniker, latest_version, arp_min_version, arp_max_version, hash, is_removed) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)"; + SQLite::Statement insertStmt = SQLite::Statement::Create(deltaConn, insertSql); + insertStmt.Bind(1, packageRowid); + insertStmt.Bind(2, id); + insertStmt.Bind(3, name); + if (moniker.empty()) insertStmt.Bind(4, nullptr); else insertStmt.Bind(4, moniker); + insertStmt.Bind(5, latestVersion); + if (arpMin.empty()) insertStmt.Bind(6, nullptr); else insertStmt.Bind(6, arpMin); + if (arpMax.empty()) insertStmt.Bind(7, nullptr); else insertStmt.Bind(7, arpMax); + if (hash.empty()) insertStmt.Bind(8, nullptr); else insertStmt.Bind(8, hash); + insertStmt.Execute(); } - else + + static constexpr std::pair s_DeltaSysRefTables[] = { + { "pfns2", "pfn" }, + { "productcodes2", "productcode" }, + { "norm_names2", "norm_name" }, + { "norm_publishers2", "norm_publisher" }, + { "upgradecodes2", "upgradecode" }, + }; + + for (const auto& [table, value] : s_DeltaSysRefTables) { - bool isNewPackage = (packageRowid == 0); - if (isNewPackage) - { - packageRowid = ++nextNewPackageRowid; - } - - AICLI_LOG(Repo, Verbose, << "Delta: recording " << (isNewPackage ? "addition" : "update") << " of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); - - { - std::string sql = "SELECT id, name, moniker, latest_version, arp_min_version, arp_max_version, hash " - "FROM packages WHERE id = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(connection, sql); - stmt.Bind(1, pkg.PackageIdentifier); - THROW_HR_IF(E_NOT_SET, !stmt.Step()); - - std::string id = stmt.GetColumn(0); - std::string name = stmt.GetColumn(1); - std::string moniker = stmt.GetColumnIsNull(2) ? "" : stmt.GetColumn(2); - std::string latestVersion = stmt.GetColumn(3); - std::string arpMin = stmt.GetColumnIsNull(4) ? "" : stmt.GetColumn(4); - std::string arpMax = stmt.GetColumnIsNull(5) ? "" : stmt.GetColumn(5); - SQLite::blob_t hash = stmt.GetColumnIsNull(6) ? SQLite::blob_t{} : stmt.GetColumn(6); - - std::string insertSql = - "INSERT OR REPLACE INTO delta_packages " - "(rowid, id, name, moniker, latest_version, arp_min_version, arp_max_version, hash, is_removed) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)"; - SQLite::Statement insertStmt = SQLite::Statement::Create(deltaConn, insertSql); - insertStmt.Bind(1, packageRowid); - insertStmt.Bind(2, id); - insertStmt.Bind(3, name); - if (moniker.empty()) insertStmt.Bind(4, nullptr); else insertStmt.Bind(4, moniker); - insertStmt.Bind(5, latestVersion); - if (arpMin.empty()) insertStmt.Bind(6, nullptr); else insertStmt.Bind(6, arpMin); - if (arpMax.empty()) insertStmt.Bind(7, nullptr); else insertStmt.Bind(7, arpMax); - if (hash.empty()) insertStmt.Bind(8, nullptr); else insertStmt.Bind(8, hash); - insertStmt.Execute(); - } - - static constexpr std::pair s_DeltaSysRefTables[] = { - { "pfns2", "pfn" }, - { "productcodes2", "productcode" }, - { "norm_names2", "norm_name" }, - { "norm_publishers2", "norm_publisher" }, - { "upgradecodes2", "upgradecode" }, - }; - - for (const auto& [table, value] : s_DeltaSysRefTables) - { - anon::ProcessDeltaSysRefTable(deltaConn, connection, baselineConn, - table, value, packageRowid, pkg.PackageIdentifier); - } - - anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, - "tags2", "tag", packageRowid, nextNewTagsRowid); - anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, - "commands2", "command", packageRowid, nextNewCommandsRowid); + anon::ProcessDeltaSysRefTable(deltaConn, connection, baselineConn, + table, value, packageRowid, pkg.PackageIdentifier); } + + anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, + "tags2", "tag", packageRowid, nextNewTagsRowid); + anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, + "commands2", "command", packageRowid, nextNewCommandsRowid); } deltaSavepoint.Commit(); diff --git a/tools/DeltaIndexTestTool/Program.cs b/tools/DeltaIndexTestTool/Program.cs index 209d2bccbf..ab8f8d5110 100644 --- a/tools/DeltaIndexTestTool/Program.cs +++ b/tools/DeltaIndexTestTool/Program.cs @@ -12,6 +12,8 @@ namespace DeltaIndexTestTool using System.IO; using System.Linq; using System.Text; + using System.Text.Json; + using System.Text.Json.Serialization; /// /// Walks the git history of a winget-pkgs clone at weekly intervals, building a full V2 index @@ -29,6 +31,7 @@ static int Main(string[] args) string branch = "master"; string resumeCommit = string.Empty; string resumeWorkingIndexPath = string.Empty; + bool autoResume = false; for (int i = 0; i < args.Length; i++) { @@ -55,6 +58,9 @@ static int Main(string[] args) case "--resume-working-index" when i + 1 < args.Length: resumeWorkingIndexPath = args[++i]; break; + case "--resume": + autoResume = true; + break; case "--help": case "-h": PrintUsage(); @@ -88,13 +94,23 @@ static int Main(string[] args) Console.Error.WriteLine($"Resume working index not found: {resumeWorkingIndexPath}"); return 1; } + if (autoResume && (!string.IsNullOrEmpty(resumeCommit) || !string.IsNullOrEmpty(resumeWorkingIndexPath))) + { + Console.Error.WriteLine("--resume cannot be combined with --resume-commit or --resume-working-index."); + return 1; + } + if (autoResume && !File.Exists(Path.Combine(outputDir, "state.json"))) + { + Console.Error.WriteLine($"No state.json found in output directory. Run without --resume first."); + return 1; + } Directory.CreateDirectory(outputDir); try { RunAnalysis(repoPath, outputDir, branch, intervalDays, maxCheckpoints, - resumeCommit, resumeWorkingIndexPath); + resumeCommit, resumeWorkingIndexPath, autoResume); return 0; } catch (Exception ex) @@ -118,10 +134,15 @@ static void PrintUsage() Console.WriteLine(" --max Maximum number of checkpoints; selects the N most"); Console.WriteLine(" recent intervals working backward from HEAD"); Console.WriteLine(" --branch Branch to walk (default: master)"); + Console.WriteLine(" --resume Resume from the last complete checkpoint recorded in"); + Console.WriteLine(" state.json (requires prior run with same --output dir)."); + Console.WriteLine(" Cannot be combined with --resume-commit."); Console.WriteLine(" --resume-commit Commit SHA to resume from"); Console.WriteLine(" --resume-working-index Path to pre-packaging working index for resume commit"); Console.WriteLine(); Console.WriteLine("Resume modes:"); + Console.WriteLine(" --resume Reads state.json from the output directory, finds the"); + Console.WriteLine(" first incomplete checkpoint, and continues from there."); Console.WriteLine(" --resume-commit only Starts a fresh index at that commit, then continues"); Console.WriteLine(" forward from there (skips re-walking older history)."); Console.WriteLine(" --resume-commit + --resume-working-index"); @@ -130,47 +151,99 @@ static void PrintUsage() Console.WriteLine(" --resume-working-index requires --resume-commit."); Console.WriteLine(); Console.WriteLine("Output:"); + Console.WriteLine(" state.json Run parameters and last complete checkpoint index (auto-managed)"); Console.WriteLine(" results.csv CSV of checkpoint sizes"); - Console.WriteLine(" report.html HTML report with comparison chart"); } static void RunAnalysis(string repoPath, string outputDir, string branch, int intervalDays, int maxCheckpoints, - string resumeCommit, string resumeWorkingIndexPath) + string resumeCommit, string resumeWorkingIndexPath, bool autoResume) { - Console.WriteLine($"Opening repository at: {repoPath}"); Console.WriteLine($"Output directory: {outputDir}"); - Console.WriteLine($"Interval: every {intervalDays} day(s)"); + string stateFilePath = Path.Combine(outputDir, "state.json"); bool hasResumeCommit = !string.IsNullOrEmpty(resumeCommit); - bool hasResumeIndex = !string.IsNullOrEmpty(resumeWorkingIndexPath); + bool hasResumeIndex = !string.IsNullOrEmpty(resumeWorkingIndexPath); - // SelectCheckpoints uses the resume commit as an anchor: it only returns commits - // strictly after it. We always prepend the resume commit as checkpoints[0] so - // that the first ApplyGitDiff starts from the resume commit itself, ensuring no - // commits between the baseline and the first selected interval are skipped. - var checkpoints = SelectCheckpoints(repoPath, branch, intervalDays, maxCheckpoints, hasResumeCommit ? resumeCommit : null); + List checkpoints; + ToolState state; + int startIndex; - if (hasResumeCommit) + if (autoResume) { - var resumeDate = LookupCommitDate(repoPath, resumeCommit); - checkpoints.Insert(0, new CommitCheckpoint(resumeCommit, resumeDate)); - } - Console.WriteLine($"Selected {checkpoints.Count} checkpoints"); + state = LoadState(stateFilePath); + checkpoints = state.Checkpoints.Select(c => new CommitCheckpoint(c.Sha, c.Date)).ToList(); + startIndex = state.LastCompleteIndex + 1; - if (checkpoints.Count == 0) + Console.WriteLine($"Opening repository at: {state.RepoPath}"); + Console.WriteLine($"Interval: every {state.IntervalDays} day(s)"); + Console.WriteLine($"Checkpoints: {checkpoints.Count} total, resuming from {startIndex}"); + + if (startIndex >= checkpoints.Count) + { + Console.WriteLine("All checkpoints are already complete. Nothing to do."); + // Re-write the CSV with all existing results so the output is consistent. + var allResults = ReconstructResults(outputDir, checkpoints); + WriteCsv(allResults, Path.Combine(outputDir, "results.csv")); + return; + } + } + else { - Console.Error.WriteLine("No checkpoints found."); - return; + Console.WriteLine($"Opening repository at: {repoPath}"); + Console.WriteLine($"Interval: every {intervalDays} day(s)"); + + checkpoints = SelectCheckpoints(repoPath, branch, intervalDays, maxCheckpoints, + hasResumeCommit ? resumeCommit : null); + + if (hasResumeCommit) + { + var resumeDate = LookupCommitDate(repoPath, resumeCommit); + checkpoints.Insert(0, new CommitCheckpoint(resumeCommit, resumeDate)); + } + + if (checkpoints.Count == 0) + { + Console.Error.WriteLine("No checkpoints found."); + return; + } + + state = new ToolState + { + RepoPath = repoPath, + Branch = branch, + IntervalDays = intervalDays, + Checkpoints = checkpoints.Select(c => new CheckpointRecord { Sha = c.Sha, Date = c.Date }).ToList(), + LastCompleteIndex = -1, + }; + SaveState(stateFilePath, state); + startIndex = 0; } + Console.WriteLine($"Selected {checkpoints.Count} checkpoints"); + string workingIndexPath = Path.Combine(outputDir, "working_index.db"); var results = new List(); var factory = new WinGetFactory(); IWinGetSQLiteIndex? workingIndex = null; + // Pre-populate results for already-complete checkpoints. + if (startIndex > 0) + { + results.AddRange(ReconstructResults(outputDir, checkpoints.Take(startIndex))); + } + + // Open the working index from the last complete checkpoint when resuming mid-run. + if (startIndex > 0) + { + string prevSavedPath = Path.Combine(outputDir, $"checkpoint_{startIndex - 1:D4}", "working_index.db"); + Console.WriteLine($"\nCopying working index from checkpoint {startIndex - 1}..."); + File.Copy(prevSavedPath, workingIndexPath, overwrite: true); + workingIndex = factory.SQLiteIndexOpen(workingIndexPath); + } + try { - for (int i = 0; i < checkpoints.Count; i++) + for (int i = startIndex; i < checkpoints.Count; i++) { var checkpoint = checkpoints[i]; Console.WriteLine($"\n[{i + 1}/{checkpoints.Count}] Processing checkpoint: {checkpoint.Sha[..8]} ({checkpoint.Date:yyyy-MM-dd})"); @@ -216,7 +289,7 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in } else if (i == 0) { - // First checkpoint with no pre-built index: build from scratch at the resume commit. + // First checkpoint with no pre-built index: build from scratch. Console.WriteLine(" Building initial full index from scratch..."); if (File.Exists(workingIndexPath)) File.Delete(workingIndexPath); @@ -287,7 +360,7 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in } File.Delete(deltaPrevWorkPath); - // Build delta index against previous full index + // Build delta index against original (checkpoint 0) full index string deltaOrigWorkPath = fullIndexPath + ".delta_orig_cp.db"; File.Copy(workingIndexPath, deltaOrigWorkPath, overwrite: true); using (var deltaPackagingIndex = factory.SQLiteIndexOpen(deltaOrigWorkPath)) @@ -313,6 +386,10 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in } results.Add(result); + + // Mark this checkpoint complete in the state file. + state.LastCompleteIndex = i; + SaveState(stateFilePath, state); } } finally @@ -325,6 +402,62 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in Console.WriteLine($"\nResults written to: {csvPath}"); } + // ----------------------------------------------------------------------- + // State file helpers + // ----------------------------------------------------------------------- + + static readonly JsonSerializerOptions s_jsonOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + static void SaveState(string path, ToolState state) + { + File.WriteAllText(path, JsonSerializer.Serialize(state, s_jsonOptions), Encoding.UTF8); + } + + static ToolState LoadState(string path) + { + string json = File.ReadAllText(path, Encoding.UTF8); + return JsonSerializer.Deserialize(json, s_jsonOptions) + ?? throw new InvalidOperationException($"Failed to deserialize state file: {path}"); + } + + /// + /// Rebuilds objects for already-complete checkpoints + /// by reading file sizes from disk. Used when resuming a run. + /// + static List ReconstructResults(string outputDir, IEnumerable checkpoints) + { + var results = new List(); + int i = 0; + foreach (var cp in checkpoints) + { + string checkpointDir = Path.Combine(outputDir, $"checkpoint_{i:D4}"); + string fullIndexPath = Path.Combine(checkpointDir, "full_index.db"); + string deltaPrevPath = Path.Combine(checkpointDir, "delta_prev.db"); + string deltaOrigPath = Path.Combine(checkpointDir, "delta_orig.db"); + + results.Add(new CheckpointResult + { + Index = i, + Date = cp.Date, + CommitSha = cp.Sha[..8], + FullIndexBytes = File.Exists(fullIndexPath) ? new FileInfo(fullIndexPath).Length : 0, + DeltaPrevBytes = File.Exists(deltaPrevPath) ? new FileInfo(deltaPrevPath).Length : 0, + DeltaOrigBytes = File.Exists(deltaOrigPath) ? new FileInfo(deltaOrigPath).Length : 0, + FullIndexPath = fullIndexPath, + PreviousFullIndexPath = i > 0 + ? Path.Combine(outputDir, $"checkpoint_{i - 1:D4}", "full_index.db") + : null, + }); + i++; + } + return results; + } + static DateTime LookupCommitDate(string repoPath, string sha) { using var repo = new Repository(repoPath); @@ -612,9 +745,28 @@ static int OpOrder((bool AnyAdded, bool AnyDeleted, bool AnyModified) f) => } else if (isPureAdd) { - string localDir = ExtractManifestDirFromTree(repo, commit, dirPath, tempDir); - TryIndexOp(index, log, commit, "add", dirPath, localDir, - (idx, dir, path) => idx.AddManifest(dir, path), ref count); + // If the directory already existed in the parent tree, this commit is + // adding new files to an existing manifest (e.g., a new locale yaml). + // The package is already in the index, so treat it as an update. + bool dirExistedInParent = parent != null && + parent[dirPath]?.TargetType == TreeEntryTargetType.Tree; + + if (dirExistedInParent) + { + string removeDir = ExtractManifestDirFromTree(repo, parent!, dirPath, tempDir); + TryIndexOp(index, log, commit, "remove", dirPath, removeDir, + (idx, dir, path) => idx.RemoveManifest(dir, path), ref count); + + string addDir = ExtractManifestDirFromTree(repo, commit, dirPath, tempDir); + TryIndexOp(index, log, commit, "add", dirPath, addDir, + (idx, dir, path) => idx.AddManifest(dir, path), ref count); + } + else + { + string localDir = ExtractManifestDirFromTree(repo, commit, dirPath, tempDir); + TryIndexOp(index, log, commit, "add", dirPath, localDir, + (idx, dir, path) => idx.AddManifest(dir, path), ref count); + } } else { @@ -698,6 +850,29 @@ static void WriteCsv(List results, string path) record CommitCheckpoint(string Sha, DateTime Date); + /// + /// Persisted run parameters and progress, written to state.json in the output directory. + /// + class ToolState + { + public string RepoPath { get; set; } = string.Empty; + public string Branch { get; set; } = string.Empty; + public int IntervalDays { get; set; } = 7; + public List Checkpoints { get; set; } = new(); + + /// + /// Index of the last fully-completed checkpoint, or -1 if none have completed. + /// Updated after each checkpoint succeeds; used by --resume to find the restart point. + /// + public int LastCompleteIndex { get; set; } = -1; + } + + class CheckpointRecord + { + public string Sha { get; set; } = string.Empty; + public DateTime Date { get; set; } + } + class CheckpointResult { public int Index { get; set; } From 01a9e536c3b4853f187faea4bae527d59208db52 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 19 Mar 2026 17:17:49 -0700 Subject: [PATCH 07/36] Fix resume --- tools/DeltaIndexTestTool/Program.cs | 56 ++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/tools/DeltaIndexTestTool/Program.cs b/tools/DeltaIndexTestTool/Program.cs index ab8f8d5110..15bb0eb96c 100644 --- a/tools/DeltaIndexTestTool/Program.cs +++ b/tools/DeltaIndexTestTool/Program.cs @@ -71,16 +71,25 @@ static int Main(string[] args) } } - if (string.IsNullOrEmpty(repoPath) || string.IsNullOrEmpty(outputDir)) + if (string.IsNullOrEmpty(outputDir)) { PrintUsage(); return 1; } - if (!Directory.Exists(repoPath)) + if (!autoResume) { - Console.Error.WriteLine($"Repository path does not exist: {repoPath}"); - return 1; + if (string.IsNullOrEmpty(repoPath)) + { + PrintUsage(); + return 1; + } + + if (!Directory.Exists(repoPath)) + { + Console.Error.WriteLine($"Repository path does not exist: {repoPath}"); + return 1; + } } // --resume-working-index requires --resume-commit; the reverse is fine (build from scratch at that commit) @@ -162,17 +171,15 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in string stateFilePath = Path.Combine(outputDir, "state.json"); bool hasResumeCommit = !string.IsNullOrEmpty(resumeCommit); - bool hasResumeIndex = !string.IsNullOrEmpty(resumeWorkingIndexPath); List checkpoints; ToolState state; - int startIndex; if (autoResume) { state = LoadState(stateFilePath); checkpoints = state.Checkpoints.Select(c => new CommitCheckpoint(c.Sha, c.Date)).ToList(); - startIndex = state.LastCompleteIndex + 1; + int startIndex = state.LastCompleteIndex + 1; Console.WriteLine($"Opening repository at: {state.RepoPath}"); Console.WriteLine($"Interval: every {state.IntervalDays} day(s)"); @@ -209,22 +216,29 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in state = new ToolState { + StateFilePath = stateFilePath, RepoPath = repoPath, Branch = branch, IntervalDays = intervalDays, Checkpoints = checkpoints.Select(c => new CheckpointRecord { Sha = c.Sha, Date = c.Date }).ToList(), LastCompleteIndex = -1, }; - SaveState(stateFilePath, state); - startIndex = 0; + SaveState(state); } + RunAnalysis(state, outputDir, resumeCommit, resumeWorkingIndexPath, checkpoints); + } + + static void RunAnalysis(ToolState state, string outputDir, string resumeCommit, string resumeWorkingIndexPath, List checkpoints) + { Console.WriteLine($"Selected {checkpoints.Count} checkpoints"); string workingIndexPath = Path.Combine(outputDir, "working_index.db"); var results = new List(); var factory = new WinGetFactory(); IWinGetSQLiteIndex? workingIndex = null; + int startIndex = state.LastCompleteIndex + 1; + bool hasResumeIndex = !string.IsNullOrEmpty(resumeWorkingIndexPath); // Pre-populate results for already-complete checkpoints. if (startIndex > 0) @@ -297,7 +311,7 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in workingIndex = factory.SQLiteIndexCreate(workingIndexPath, 2u, 1u); workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, "0"); - int added = AddAllManifests(workingIndex, repoPath, checkpoint.Sha); + int added = AddAllManifests(workingIndex, state.RepoPath, checkpoint.Sha); Console.WriteLine($" Added {added} manifest files"); workingIndex.Dispose(); @@ -329,7 +343,7 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in string origFullIndexPath = results[0].FullIndexPath!; Console.WriteLine(" Applying git diff from previous checkpoint..."); - int changed = ApplyGitDiff(workingIndex!, repoPath, prevCheckpoint.Sha, checkpoint.Sha, checkpointDir); + int changed = ApplyGitDiff(workingIndex!, state.RepoPath, prevCheckpoint.Sha, checkpoint.Sha, checkpointDir); Console.WriteLine($" Applied {changed} manifest changes"); workingIndex!.Dispose(); @@ -389,7 +403,7 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in // Mark this checkpoint complete in the state file. state.LastCompleteIndex = i; - SaveState(stateFilePath, state); + SaveState(state); } } finally @@ -413,16 +427,18 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; - static void SaveState(string path, ToolState state) + static void SaveState(ToolState state) { - File.WriteAllText(path, JsonSerializer.Serialize(state, s_jsonOptions), Encoding.UTF8); + File.WriteAllText(state.StateFilePath, JsonSerializer.Serialize(state, s_jsonOptions), Encoding.UTF8); } static ToolState LoadState(string path) { string json = File.ReadAllText(path, Encoding.UTF8); - return JsonSerializer.Deserialize(json, s_jsonOptions) + var result = JsonSerializer.Deserialize(json, s_jsonOptions) ?? throw new InvalidOperationException($"Failed to deserialize state file: {path}"); + result.StateFilePath = path; + return result; } /// @@ -782,6 +798,8 @@ static int OpOrder((bool AnyAdded, bool AnyDeleted, bool AnyModified) f) => } } + Directory.Delete(tempDir, true); + return count; } @@ -797,8 +815,9 @@ static void TryIndexOp(IWinGetSQLiteIndex index, StreamWriter log, Commit commit } catch (Exception ex) { - log.WriteLine($"{commit.Sha}\t{commit.Author.When:yyyy-MM-dd HH:mm:ss zzz}\t{operationName}\t{dirPath}\t{ex.HResult}"); - Console.Error.WriteLine($" Failed to {operationName} manifest '{dirPath}': {ex.HResult}"); + int result = ex.InnerException?.HResult ?? ex.HResult; + log.WriteLine($"{commit.Sha}\t{commit.Author.When:yyyy-MM-dd HH:mm:ss zzz}\t{operationName}\t{dirPath}\t{result}"); + Console.Error.WriteLine($" Failed to {operationName} manifest '{dirPath}': {result}"); } } @@ -855,6 +874,9 @@ record CommitCheckpoint(string Sha, DateTime Date); /// class ToolState { + [JsonIgnore] + public string StateFilePath { get; set; } = string.Empty; + public string RepoPath { get; set; } = string.Empty; public string Branch { get; set; } = string.Empty; public int IntervalDays { get; set; } = 7; From ee327f26496542c4b4594195af5f5bf55a8b4907 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 23 Mar 2026 11:42:06 -0700 Subject: [PATCH 08/36] Add compressed file sizes --- tools/DeltaIndexTestTool/Program.cs | 130 +++++++++++++++++++++++----- 1 file changed, 109 insertions(+), 21 deletions(-) diff --git a/tools/DeltaIndexTestTool/Program.cs b/tools/DeltaIndexTestTool/Program.cs index 15bb0eb96c..51bf508652 100644 --- a/tools/DeltaIndexTestTool/Program.cs +++ b/tools/DeltaIndexTestTool/Program.cs @@ -10,6 +10,7 @@ namespace DeltaIndexTestTool using System.Collections.Generic; using System.Diagnostics; using System.IO; + using System.IO.Compression; using System.Linq; using System.Text; using System.Text.Json; @@ -32,6 +33,7 @@ static int Main(string[] args) string resumeCommit = string.Empty; string resumeWorkingIndexPath = string.Empty; bool autoResume = false; + bool recomputeCompressed = false; for (int i = 0; i < args.Length; i++) { @@ -61,6 +63,9 @@ static int Main(string[] args) case "--resume": autoResume = true; break; + case "--recompute-compressed": + recomputeCompressed = true; + break; case "--help": case "-h": PrintUsage(); @@ -108,18 +113,23 @@ static int Main(string[] args) Console.Error.WriteLine("--resume cannot be combined with --resume-commit or --resume-working-index."); return 1; } - if (autoResume && !File.Exists(Path.Combine(outputDir, "state.json"))) + if ((autoResume || recomputeCompressed) && !File.Exists(Path.Combine(outputDir, "state.json"))) { Console.Error.WriteLine($"No state.json found in output directory. Run without --resume first."); return 1; } + if (recomputeCompressed && !autoResume) + { + Console.Error.WriteLine("--recompute-compressed requires --resume."); + return 1; + } Directory.CreateDirectory(outputDir); try { RunAnalysis(repoPath, outputDir, branch, intervalDays, maxCheckpoints, - resumeCommit, resumeWorkingIndexPath, autoResume); + resumeCommit, resumeWorkingIndexPath, autoResume, recomputeCompressed); return 0; } catch (Exception ex) @@ -146,6 +156,10 @@ static void PrintUsage() Console.WriteLine(" --resume Resume from the last complete checkpoint recorded in"); Console.WriteLine(" state.json (requires prior run with same --output dir)."); Console.WriteLine(" Cannot be combined with --resume-commit."); + Console.WriteLine(" --recompute-compressed Used with --resume: recomputes compressed file sizes for"); + Console.WriteLine(" all already-completed checkpoints before continuing."); + Console.WriteLine(" Use this to backfill compressed sizes into a run that"); + Console.WriteLine(" completed before compression measurement was added."); Console.WriteLine(" --resume-commit Commit SHA to resume from"); Console.WriteLine(" --resume-working-index Path to pre-packaging working index for resume commit"); Console.WriteLine(); @@ -165,7 +179,7 @@ static void PrintUsage() } static void RunAnalysis(string repoPath, string outputDir, string branch, int intervalDays, int maxCheckpoints, - string resumeCommit, string resumeWorkingIndexPath, bool autoResume) + string resumeCommit, string resumeWorkingIndexPath, bool autoResume, bool recomputeCompressed) { Console.WriteLine($"Output directory: {outputDir}"); @@ -185,12 +199,37 @@ static void RunAnalysis(string repoPath, string outputDir, string branch, int in Console.WriteLine($"Interval: every {state.IntervalDays} day(s)"); Console.WriteLine($"Checkpoints: {checkpoints.Count} total, resuming from {startIndex}"); + if (recomputeCompressed && startIndex > 0) + { + Console.WriteLine($"\nRecomputing compressed sizes for {startIndex} completed checkpoint(s)..."); + for (int ci = 0; ci < startIndex; ci++) + { + string cpDir = Path.Combine(outputDir, $"checkpoint_{ci:D4}"); + string fullPath = Path.Combine(cpDir, "full_index.db"); + string prevPath = Path.Combine(cpDir, "delta_prev.db"); + string origPath = Path.Combine(cpDir, "delta_orig.db"); + + var rec = state.Checkpoints[ci]; + rec.FullIndexCompressedBytes = File.Exists(fullPath) ? GetCompressedSize(fullPath) : 0; + rec.DeltaPrevCompressedBytes = File.Exists(prevPath) ? GetCompressedSize(prevPath) : 0; + rec.DeltaOrigCompressedBytes = File.Exists(origPath) ? GetCompressedSize(origPath) : 0; + + Console.WriteLine($" [{ci + 1}/{startIndex}] checkpoint_{ci:D4}: " + + $"full={rec.FullIndexCompressedBytes / 1024.0 / 1024.0:F2} MB " + + $"prev={rec.DeltaPrevCompressedBytes / 1024.0 / 1024.0:F2} MB " + + $"orig={rec.DeltaOrigCompressedBytes / 1024.0 / 1024.0:F2} MB"); + } + SaveState(state); + Console.WriteLine(" Compressed sizes saved to state.json"); + } + if (startIndex >= checkpoints.Count) { Console.WriteLine("All checkpoints are already complete. Nothing to do."); // Re-write the CSV with all existing results so the output is consistent. - var allResults = ReconstructResults(outputDir, checkpoints); + var allResults = ReconstructResults(outputDir, state.Checkpoints); WriteCsv(allResults, Path.Combine(outputDir, "results.csv")); + Console.WriteLine($"Results written to: {Path.Combine(outputDir, "results.csv")}"); return; } } @@ -243,7 +282,7 @@ static void RunAnalysis(ToolState state, string outputDir, string resumeCommit, // Pre-populate results for already-complete checkpoints. if (startIndex > 0) { - results.AddRange(ReconstructResults(outputDir, checkpoints.Take(startIndex))); + results.AddRange(ReconstructResults(outputDir, state.Checkpoints.Take(startIndex))); } // Open the working index from the last complete checkpoint when resuming mid-run. @@ -296,10 +335,11 @@ static void RunAnalysis(ToolState state, string outputDir, string resumeCommit, workingIndex = factory.SQLiteIndexOpen(workingIndexPath); result.FullIndexBytes = new FileInfo(fullIndexPath).Length; + result.FullIndexCompressedBytes = GetCompressedSize(fullIndexPath); result.PreviousFullIndexPath = null; result.FullIndexPath = fullIndexPath; - Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB"); + Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB ({result.FullIndexCompressedBytes / 1024.0 / 1024.0:F2} MB compressed)"); } else if (i == 0) { @@ -330,10 +370,11 @@ static void RunAnalysis(ToolState state, string outputDir, string resumeCommit, workingIndex = factory.SQLiteIndexOpen(workingIndexPath); result.FullIndexBytes = new FileInfo(fullIndexPath).Length; + result.FullIndexCompressedBytes = GetCompressedSize(fullIndexPath); result.PreviousFullIndexPath = null; result.FullIndexPath = fullIndexPath; - Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB"); + Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB ({result.FullIndexCompressedBytes / 1024.0 / 1024.0:F2} MB compressed)"); } else { @@ -391,17 +432,25 @@ static void RunAnalysis(ToolState state, string outputDir, string resumeCommit, result.FullIndexBytes = new FileInfo(fullIndexPath).Length; result.DeltaPrevBytes = File.Exists(deltaPrevPath) ? new FileInfo(deltaPrevPath).Length : 0; result.DeltaOrigBytes = File.Exists(deltaOrigPath) ? new FileInfo(deltaOrigPath).Length : 0; + result.FullIndexCompressedBytes = GetCompressedSize(fullIndexPath); + result.DeltaPrevCompressedBytes = File.Exists(deltaPrevPath) ? GetCompressedSize(deltaPrevPath) : 0; + result.DeltaOrigCompressedBytes = File.Exists(deltaOrigPath) ? GetCompressedSize(deltaOrigPath) : 0; result.PreviousFullIndexPath = prevFullIndexPath; result.FullIndexPath = fullIndexPath; - Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB"); - Console.WriteLine($" Delta prev: {result.DeltaPrevBytes / 1024.0 / 1024.0:F2} MB"); - Console.WriteLine($" Delta orig: {result.DeltaOrigBytes / 1024.0 / 1024.0:F2} MB"); + Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB ({result.FullIndexCompressedBytes / 1024.0 / 1024.0:F2} MB compressed)"); + Console.WriteLine($" Delta prev: {result.DeltaPrevBytes / 1024.0 / 1024.0:F2} MB ({result.DeltaPrevCompressedBytes / 1024.0 / 1024.0:F2} MB compressed)"); + Console.WriteLine($" Delta orig: {result.DeltaOrigBytes / 1024.0 / 1024.0:F2} MB ({result.DeltaOrigCompressedBytes / 1024.0 / 1024.0:F2} MB compressed)"); } results.Add(result); - // Mark this checkpoint complete in the state file. + // Mark this checkpoint complete in the state file, persisting compressed sizes + // so --resume can reconstruct results without re-compressing. + var rec = state.Checkpoints[i]; + rec.FullIndexCompressedBytes = result.FullIndexCompressedBytes; + rec.DeltaPrevCompressedBytes = result.DeltaPrevCompressedBytes; + rec.DeltaOrigCompressedBytes = result.DeltaOrigCompressedBytes; state.LastCompleteIndex = i; SaveState(state); } @@ -443,13 +492,14 @@ static ToolState LoadState(string path) /// /// Rebuilds objects for already-complete checkpoints - /// by reading file sizes from disk. Used when resuming a run. + /// by reading file sizes from disk and compressed sizes from the persisted state records. + /// Used when resuming a run. /// - static List ReconstructResults(string outputDir, IEnumerable checkpoints) + static List ReconstructResults(string outputDir, IEnumerable records) { var results = new List(); int i = 0; - foreach (var cp in checkpoints) + foreach (var rec in records) { string checkpointDir = Path.Combine(outputDir, $"checkpoint_{i:D4}"); string fullIndexPath = Path.Combine(checkpointDir, "full_index.db"); @@ -459,11 +509,14 @@ static List ReconstructResults(string outputDir, IEnumerable 0 ? Path.Combine(outputDir, $"checkpoint_{i - 1:D4}", "full_index.db") @@ -851,18 +904,47 @@ static string ExtractManifestDirFromTree(Repository repo, Commit commit, string return localDir; } + /// + /// Compresses using Deflate (the same algorithm used by MSIX/ZIP + /// packaging) and returns the compressed byte count. A temporary file is used so that + /// large files are not loaded into memory. The temporary file is always deleted on return. + /// + static long GetCompressedSize(string filePath) + { + string tempPath = filePath + ".compressed_measure.zip"; + try + { + using (var zipStream = File.Create(tempPath)) + using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, leaveOpen: false)) + { + var entry = archive.CreateEntry(Path.GetFileName(filePath), CompressionLevel.Optimal); + using var entryStream = entry.Open(); + using var sourceStream = File.OpenRead(filePath); + sourceStream.CopyTo(entryStream); + } + return new FileInfo(tempPath).Length; + } + finally + { + if (File.Exists(tempPath)) File.Delete(tempPath); + } + } + static void WriteCsv(List results, string path) { using var writer = new StreamWriter(path, false, Encoding.UTF8); - writer.WriteLine("Index,Date,CommitSha,FullIndexMB,DeltaPrevMB,DeltaOrigMB"); + writer.WriteLine("Index,Date,CommitSha,FullIndexMB,DeltaPrevMB,DeltaOrigMB,FullIndexCompressedMB,DeltaPrevCompressedMB,DeltaOrigCompressedMB"); foreach (var r in results) { - double fullMb = r.FullIndexBytes / 1024.0 / 1024.0; - double deltaPrevMb = r.DeltaPrevBytes / 1024.0 / 1024.0; - double deltaOrigMb = r.DeltaOrigBytes / 1024.0 / 1024.0; + double fullMb = r.FullIndexBytes / 1024.0 / 1024.0; + double deltaPrevMb = r.DeltaPrevBytes / 1024.0 / 1024.0; + double deltaOrigMb = r.DeltaOrigBytes / 1024.0 / 1024.0; + double fullCMb = r.FullIndexCompressedBytes / 1024.0 / 1024.0; + double deltaPrevCMb = r.DeltaPrevCompressedBytes / 1024.0 / 1024.0; + double deltaOrigCMb = r.DeltaOrigCompressedBytes / 1024.0 / 1024.0; - writer.WriteLine($"{r.Index},{r.Date:yyyy-MM-dd},{r.CommitSha},{fullMb:F2},{deltaPrevMb:F2},{deltaOrigMb:F2}"); + writer.WriteLine($"{r.Index},{r.Date:yyyy-MM-dd},{r.CommitSha},{fullMb:F2},{deltaPrevMb:F2},{deltaOrigMb:F2},{fullCMb:F2},{deltaPrevCMb:F2},{deltaOrigCMb:F2}"); } } } @@ -893,6 +975,9 @@ class CheckpointRecord { public string Sha { get; set; } = string.Empty; public DateTime Date { get; set; } + public long FullIndexCompressedBytes { get; set; } + public long DeltaPrevCompressedBytes { get; set; } + public long DeltaOrigCompressedBytes { get; set; } } class CheckpointResult @@ -903,6 +988,9 @@ class CheckpointResult public long FullIndexBytes { get; set; } public long DeltaPrevBytes { get; set; } public long DeltaOrigBytes { get; set; } + public long FullIndexCompressedBytes { get; set; } + public long DeltaPrevCompressedBytes { get; set; } + public long DeltaOrigCompressedBytes { get; set; } public string? FullIndexPath { get; set; } public string? PreviousFullIndexPath { get; set; } } From 5098b67de1d19cb402ca8a0299c3f49131104a4b Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 23 Mar 2026 15:43:37 -0700 Subject: [PATCH 09/36] Analysis script; attempt to determine best baseline interval from data; needs more review --- tools/DeltaIndexTestTool/analyze.py | 397 ++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 tools/DeltaIndexTestTool/analyze.py diff --git a/tools/DeltaIndexTestTool/analyze.py b/tools/DeltaIndexTestTool/analyze.py new file mode 100644 index 0000000000..3c8ea21a63 --- /dev/null +++ b/tools/DeltaIndexTestTool/analyze.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +""" +analyze.py - Baseline refresh timing optimizer for winget delta indexes. + +Reads a results.csv produced by DeltaIndexTestTool and models the total compressed +egress across multiple user updates under different baseline refresh schedules. +Sweeps all candidate refresh periods and recommends the one that minimizes total +expected compressed egress given an assumed user staleness distribution. + +Key approximation: the measured delta_orig_compressed growth curve (from baseline 0) +is used as a proxy for delta growth from any hypothetical baseline. This holds when +the repository grows at a roughly steady rate over time. + +Usage: + python analyze.py --csv results.csv --distribution weekly + python analyze.py --csv results.csv --distribution distribution.json + python analyze.py --csv results.csv --distribution weekly --egress-cost-per-gb 0.087 --output-chart chart.png +""" + +import argparse +import csv +import json +import sys +from datetime import datetime +from pathlib import Path + +# --------------------------------------------------------------------------- +# Built-in staleness distribution presets +# --------------------------------------------------------------------------- +# Each preset is a PMF over "days since last update" at the moment a user +# triggers an update. Weights must sum to 1.0. +# Replace buckets with telemetry-derived data when available — no other code +# changes are needed; supply a JSON file matching this format via --distribution. + +PRESETS = { + "daily_heavy": { + "description": "Assumption: heavy daily-update user base (CI systems, power users)", + "buckets": [ + {"days": 1, "weight": 0.40}, + {"days": 7, "weight": 0.35}, + {"days": 30, "weight": 0.20}, + {"days": 90, "weight": 0.05}, + ], + }, + "weekly": { + "description": "Assumption: mostly weekly updaters (typical developer)", + "buckets": [ + {"days": 1, "weight": 0.10}, + {"days": 7, "weight": 0.50}, + {"days": 30, "weight": 0.30}, + {"days": 90, "weight": 0.10}, + ], + }, + "monthly": { + "description": "Assumption: mostly monthly or infrequent updaters", + "buckets": [ + {"days": 1, "weight": 0.05}, + {"days": 7, "weight": 0.20}, + {"days": 30, "weight": 0.45}, + {"days": 90, "weight": 0.30}, + ], + }, +} + + +# --------------------------------------------------------------------------- +# Data loading +# --------------------------------------------------------------------------- + +def load_distribution(dist_arg): + """Return a distribution dict from a preset name or a JSON file path.""" + if dist_arg in PRESETS: + return PRESETS[dist_arg] + p = Path(dist_arg) + if not p.exists(): + raise FileNotFoundError(f"Distribution file not found: {dist_arg}") + with p.open(encoding="utf-8") as f: + return json.load(f) + + +def load_csv(csv_path): + """ + Load checkpoints from results.csv. Returns a list of dicts (one per row). + If compressed-size columns are absent (produced before that feature was added), + falls back to the uncompressed values so older CSVs remain usable. + """ + rows = [] + with open(csv_path, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + r = {} + r["Date"] = datetime.strptime(row["Date"].strip(), "%Y-%m-%d") + r["CommitSha"] = row.get("CommitSha", "").strip() + for col in ("FullIndexMB", "DeltaPrevMB", "DeltaOrigMB"): + r[col] = float(row.get(col) or 0) + # Compressed columns — fall back gracefully to uncompressed values. + r["FullIndexCompressedMB"] = float( + row.get("FullIndexCompressedMB") or r["FullIndexMB"]) + r["DeltaPrevCompressedMB"] = float( + row.get("DeltaPrevCompressedMB") or r["DeltaPrevMB"]) + r["DeltaOrigCompressedMB"] = float( + row.get("DeltaOrigCompressedMB") or r["DeltaOrigMB"]) + rows.append(r) + return rows + + +# --------------------------------------------------------------------------- +# Cost model +# --------------------------------------------------------------------------- + +def compute_interval_days(checkpoints): + """Estimate the average days between consecutive checkpoints.""" + if len(checkpoints) < 2: + return 7 + span = (checkpoints[-1]["Date"] - checkpoints[0]["Date"]).days + return span / (len(checkpoints) - 1) + + +def normalize_buckets(buckets): + """Return a copy of buckets with weights normalized to sum to 1.0.""" + total = sum(b["weight"] for b in buckets) + if abs(total - 1.0) > 0.01: + print(f"Warning: distribution weights sum to {total:.3f}, normalizing to 1.0", + file=sys.stderr) + return [{"days": b["days"], "weight": b["weight"] / total} for b in buckets] + + +def prob_stale_more_than(days_threshold, buckets): + """ + P(user is more than `days_threshold` days stale when they update). + Equals the sum of weights for all buckets with days > days_threshold. + """ + return sum(b["weight"] for b in buckets if b["days"] > days_threshold) + + +def simulate_schedule(checkpoints, period, buckets, interval_days): + """ + Simulate a periodic baseline refresh every `period` checkpoints and return + the total expected compressed egress in MB across all checkpoints. + + At each checkpoint i, baseline age a = i % period: + + delta_cost = DeltaOrigCompressedMB[a] (measured growth curve, used as + proxy for any baseline period) + p_need_baseline = P(user stale > a * interval_days) + baseline_cost = FullIndexCompressedMB at the most recent baseline + + expected_cost_i = delta_cost + p_need_baseline * baseline_cost + + When a = 0 (baseline just published): delta_cost = 0, p_need_baseline = 1.0, + so every updating user downloads the full baseline — correct by construction. + + The total is summed across all N checkpoints; it is proportional to total + egress (one representative user update per checkpoint is assumed). + """ + delta_curve = [cp["DeltaOrigCompressedMB"] for cp in checkpoints] + n = len(checkpoints) + + total_mb = 0.0 + for i, cp in enumerate(checkpoints): + age = i % period + baseline_idx = i - age + + delta_cost = delta_curve[age] if age < len(delta_curve) else delta_curve[-1] + baseline_cost = checkpoints[baseline_idx]["FullIndexCompressedMB"] + p_baseline = prob_stale_more_than(age * interval_days, buckets) + + total_mb += delta_cost + p_baseline * baseline_cost + + return total_mb + + +def find_crossover(checkpoints, threshold): + """ + Return the index of the first checkpoint where + DeltaOrigCompressedMB / FullIndexCompressedMB >= threshold, or None. + """ + for i, cp in enumerate(checkpoints): + full = cp["FullIndexCompressedMB"] + if full > 0 and cp["DeltaOrigCompressedMB"] / full >= threshold: + return i + return None + + +# --------------------------------------------------------------------------- +# Formatting helpers +# --------------------------------------------------------------------------- + +def fmt_mb(mb): + """Format a MB value as MB or GB depending on magnitude.""" + if mb >= 1024: + return f"{mb / 1024:.2f} GB" + return f"{mb:.1f} MB" + + +def fmt_days(days): + """Format a number of days as 'd' or 'wk' shorthand.""" + if days % 7 == 0 and days >= 7: + return f"~{int(days // 7)}wk" + return f"~{days:.0f}d" + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser( + description="Optimize baseline refresh timing for winget delta indexes.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="Built-in distribution presets: " + ", ".join(PRESETS), + ) + parser.add_argument("--csv", required=True, + help="Path to results.csv from DeltaIndexTestTool") + parser.add_argument("--distribution", required=True, + help="Staleness distribution: preset name or path to JSON file. " + f"Presets: {', '.join(PRESETS)}") + parser.add_argument("--egress-cost-per-gb", type=float, default=None, dest="cost_per_gb", + help="Optional egress cost per GB. If omitted, output is in bytes only.") + parser.add_argument("--output-chart", default=None, dest="output_chart", + help="Optional path to save a chart image (e.g. chart.png). Requires matplotlib.") + args = parser.parse_args() + + # --- Load inputs -------------------------------------------------------- + checkpoints = load_csv(args.csv) + if not checkpoints: + print("Error: no checkpoints in CSV.", file=sys.stderr) + sys.exit(1) + + dist = load_distribution(args.distribution) + buckets = normalize_buckets(dist["buckets"]) + description = dist.get("description", args.distribution) + + interval_days = compute_interval_days(checkpoints) + n = len(checkpoints) + + # --- Header ------------------------------------------------------------- + print(f"\n{'='*68}") + print(f" Baseline Timing Analysis") + print(f"{'='*68}") + print(f" CSV: {args.csv}") + print(f" Checkpoints: {n} " + f"({checkpoints[0]['Date']:%Y-%m-%d} to {checkpoints[-1]['Date']:%Y-%m-%d})") + print(f" Avg interval: {interval_days:.1f} days " + f"(total span: {(checkpoints[-1]['Date'] - checkpoints[0]['Date']).days} days)") + print(f" Distribution: {description}") + if args.cost_per_gb is not None: + print(f" Egress cost: ${args.cost_per_gb:.4f}/GB") + print() + + # --- Delta growth crossovers ------------------------------------------- + c50 = find_crossover(checkpoints, 0.50) + c100 = find_crossover(checkpoints, 1.00) + print(" Delta (compressed) growth from baseline:") + if c50 is not None: + print(f" Exceeds 50% of full index at checkpoint {c50:>3} " + f"({fmt_days(c50 * interval_days)})") + else: + print(" Never exceeds 50% of full index within measured period") + if c100 is not None: + print(f" Exceeds 100% of full index at checkpoint {c100:>3} " + f"({fmt_days(c100 * interval_days)})") + else: + print(" Never exceeds 100% of full index within measured period") + print() + + # --- Simulate all periods ----------------------------------------------- + results = [] + for period in range(1, n + 1): + total_mb = simulate_schedule(checkpoints, period, buckets, interval_days) + results.append({ + "period": period, + "period_days": period * interval_days, + "total_mb": total_mb, + "avg_mb": total_mb / n, + }) + + optimal = min(results, key=lambda r: r["total_mb"]) + always_full = results[0] # period == 1 + never_refresh = results[-1] # period == n + + # --- Determine which periods to print in the table ---------------------- + # Always show: period 1, optimal, and period n. + # Also show a sample of ~15-20 evenly-spaced periods in between. + show = {1, optimal["period"], n} + step = max(1, n // 18) + for p in range(step, n, step): + show.add(p) + + # --- Print table -------------------------------------------------------- + print(f"--- Schedule Comparison (total compressed egress across {n} checkpoints) ---") + col_cost = args.cost_per_gb is not None + hdr = (f" {'Period':>6} {'Interval':>9} {'Total Egress':>14} {'Avg/Checkpoint':>16}") + if col_cost: + hdr += f" {'Total Cost':>12}" + print(hdr) + sep = f" {'-'*6} {'-'*9} {'-'*14} {'-'*16}" + (f" {'-'*12}" if col_cost else "") + print(sep) + + for r in sorted(results, key=lambda r: r["period"]): + if r["period"] not in show: + continue + tag = "" + if r["period"] == 1: + tag = " (always full)" + elif r["period"] == n: + tag = " (never refresh)" + elif r["period"] == optimal["period"]: + tag = " ← optimal" + + line = (f" {r['period']:>6} {fmt_days(r['period_days']):>9} " + f"{fmt_mb(r['total_mb']):>14} {fmt_mb(r['avg_mb']):>16}{tag}") + if col_cost: + cost = r["total_mb"] / 1024 * args.cost_per_gb + line += f" ${cost:>11.2f}" + print(line) + + print() + + # --- Recommendation summary --------------------------------------------- + savings_vs_full = 100.0 * (1.0 - optimal["total_mb"] / always_full["total_mb"]) + savings_vs_never = (100.0 * (1.0 - optimal["total_mb"] / never_refresh["total_mb"]) + if never_refresh["total_mb"] > 0 else 0.0) + + print(f" Recommendation: refresh baseline every {optimal['period']} checkpoint(s) " + f"({fmt_days(optimal['period_days'])})") + print(f" Total egress: {fmt_mb(optimal['total_mb'])} " + f"(avg {fmt_mb(optimal['avg_mb'])} per checkpoint)") + print(f" vs always-full: {savings_vs_full:+.1f}%") + if optimal["period"] < n: + print(f" vs never-refresh: {savings_vs_never:+.1f}%") + if col_cost: + opt_cost = optimal["total_mb"] / 1024 * args.cost_per_gb + full_cost = always_full["total_mb"] / 1024 * args.cost_per_gb + print(f" Estimated cost: ${opt_cost:.2f} (vs ${full_cost:.2f} always-full)") + print() + + # --- Chart -------------------------------------------------------------- + if args.output_chart: + _write_chart(args.output_chart, checkpoints, optimal, interval_days, + description, always_full, never_refresh) + + +def _write_chart(path, checkpoints, optimal, interval_days, description, + always_full, never_refresh): + try: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + import matplotlib.ticker as ticker + except ImportError: + print(" Note: matplotlib not available; skipping chart. " + "Install with: pip install matplotlib", file=sys.stderr) + return + + dates = [cp["Date"] for cp in checkpoints] + full_vals = [cp["FullIndexCompressedMB"] for cp in checkpoints] + delta_vals = [cp["DeltaOrigCompressedMB"] for cp in checkpoints] + n = len(checkpoints) + + fig, ax = plt.subplots(figsize=(13, 6)) + + ax.plot(dates, full_vals, label="Full index (compressed)", color="#C0392B", linewidth=2) + ax.plot(dates, delta_vals, label="Delta from baseline (compressed)", color="#27AE60", linewidth=2) + + # Vertical lines at recommended baseline refresh points + opt_period = optimal["period"] + baseline_indices = list(range(opt_period, n, opt_period)) + first_line = True + for bi in baseline_indices: + lbl = (f"Recommended baseline ({fmt_days(opt_period * interval_days)} period)" + if first_line else None) + ax.axvline(x=checkpoints[bi]["Date"], color="#2980B9", + linestyle="--", linewidth=0.9, alpha=0.7, label=lbl) + first_line = False + + # Shade the area between the curves for visual clarity + ax.fill_between(dates, delta_vals, full_vals, + where=[d < f for d, f in zip(delta_vals, full_vals)], + alpha=0.07, color="#27AE60", label="Potential savings region") + + ax.set_xlabel("Date") + ax.set_ylabel("Compressed Size (MB)") + ax.set_title(f"Delta Index Baseline Timing Analysis\nDistribution: {description}") + ax.legend(loc="upper left") + ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f"{x:.0f} MB")) + fig.autofmt_xdate() + plt.tight_layout() + plt.savefig(path, dpi=150) + plt.close(fig) + print(f" Chart saved to: {path}") + + +if __name__ == "__main__": + main() From a16b7fdbf05fcdbbea422b163cac9fec38291b12 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 23 Mar 2026 16:44:11 -0700 Subject: [PATCH 10/36] Update script to properly simulate download frequency --- tools/DeltaIndexTestTool/analyze.py | 110 ++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 32 deletions(-) diff --git a/tools/DeltaIndexTestTool/analyze.py b/tools/DeltaIndexTestTool/analyze.py index 3c8ea21a63..3fdc456b02 100644 --- a/tools/DeltaIndexTestTool/analyze.py +++ b/tools/DeltaIndexTestTool/analyze.py @@ -9,9 +9,22 @@ Sweeps all candidate refresh periods and recommends the one that minimizes total expected compressed egress given an assumed user staleness distribution. -Key approximation: the measured delta_orig_compressed growth curve (from baseline 0) -is used as a proxy for delta growth from any hypothetical baseline. This holds when -the repository grows at a roughly steady rate over time. +COST MODEL SUMMARY +------------------ +For a refresh period of P checkpoints, the baseline age cycles 0..P-1 in steady state. +For each user type (D days between updates, population weight W): + + - Avg delta cost per download = average of DeltaOrig[0..P-1] + (user downloads current delta-from-baseline regardless of personal staleness) + - P(needs new baseline) = min(D, P*interval_days) / (P*interval_days) + (fraction of time the user's last update predates the current baseline) + - Downloads per user over window = window_days / D + (daily users contribute 7x more download events than weekly users) + +Total egress = sum over buckets of: W * (window/D) * (avg_delta + p_baseline * full_avg) + +Key approximation: DeltaOrig growth from baseline 0 is used as a proxy for delta +growth from any hypothetical baseline (reasonable when repository growth is steady). Usage: python analyze.py --csv results.csv --distribution weekly @@ -127,47 +140,79 @@ def normalize_buckets(buckets): return [{"days": b["days"], "weight": b["weight"] / total} for b in buckets] -def prob_stale_more_than(days_threshold, buckets): - """ - P(user is more than `days_threshold` days stale when they update). - Equals the sum of weights for all buckets with days > days_threshold. - """ - return sum(b["weight"] for b in buckets if b["days"] > days_threshold) - - def simulate_schedule(checkpoints, period, buckets, interval_days): """ Simulate a periodic baseline refresh every `period` checkpoints and return - the total expected compressed egress in MB across all checkpoints. + the total expected compressed egress in MB across the measurement window. + + MODEL + ----- + For a given refresh period P (checkpoints), baseline age cycles 0..P-1. + When a user updates at a moment when the baseline is `a` checkpoints old: - At each checkpoint i, baseline age a = i % period: + - They always download the current delta: DeltaOrig[a] + - If they are MORE stale than the baseline (their last update was before + the baseline was published), they additionally download the full index. - delta_cost = DeltaOrigCompressedMB[a] (measured growth curve, used as - proxy for any baseline period) - p_need_baseline = P(user stale > a * interval_days) - baseline_cost = FullIndexCompressedMB at the most recent baseline + The probability that a user of type D (days between updates) needs the + baseline at a random update moment equals the fraction of the baseline + cycle during which the baseline is newer than the user's last update: - expected_cost_i = delta_cost + p_need_baseline * baseline_cost + p_needs_baseline(D) = min(D, period_days) / period_days - When a = 0 (baseline just published): delta_cost = 0, p_need_baseline = 1.0, - so every updating user downloads the full baseline — correct by construction. + This is derived from: the baseline was published `a` days ago; user needs + it if a < D; in steady state `a` is uniform over [0, period_days), so the + probability is min(D, period_days) / period_days. - The total is summed across all N checkpoints; it is proportional to total - egress (one representative user update per checkpoint is assumed). + The average delta cost across a complete baseline cycle is the mean of + DeltaOrig[0..P-1], since in steady state the baseline is equally likely + to be at any age. + + FREQUENCY CORRECTION + -------------------- + A user who updates every D days generates window_days/D download events + over the measurement window — not one per checkpoint. The total egress + contribution of each user type is therefore: + + W[D] * (window_days / D) * (cycle_avg_delta + p_needs_baseline * full_avg) + + where W[D] is the population fraction for that update frequency. Summing + across all user types gives the total expected egress for the window. + + This correctly accounts for the fact that daily updaters contribute far + more download events than monthly updaters, even though each individual + download for a daily updater is cheaper (smaller delta, less likely to + need a baseline reset). """ - delta_curve = [cp["DeltaOrigCompressedMB"] for cp in checkpoints] - n = len(checkpoints) + delta_curve = [cp["DeltaOrigCompressedMB"] for cp in checkpoints] + n = len(checkpoints) + period_days = period * interval_days + window_days = max((n - 1) * interval_days, interval_days) + + # Average full index size over the measurement window (baselines are + # published at various sizes as the index grows; use the mean as the + # representative cost of downloading a baseline). + full_avg = sum(cp["FullIndexCompressedMB"] for cp in checkpoints) / n + + # Average delta cost across a complete baseline cycle. + # DeltaOrig[a] is the measured delta growth from baseline 0; used here as + # a proxy for delta growth from any baseline (the key approximation). + cycle_deltas = [delta_curve[min(a, len(delta_curve) - 1)] for a in range(period)] + cycle_avg_delta = sum(cycle_deltas) / period total_mb = 0.0 - for i, cp in enumerate(checkpoints): - age = i % period - baseline_idx = i - age + for bucket in buckets: + D = bucket["days"] + W = bucket["weight"] + + # Number of download events from this user type over the window. + num_downloads = window_days / D - delta_cost = delta_curve[age] if age < len(delta_curve) else delta_curve[-1] - baseline_cost = checkpoints[baseline_idx]["FullIndexCompressedMB"] - p_baseline = prob_stale_more_than(age * interval_days, buckets) + # Fraction of those downloads that require a new baseline. + p_needs_baseline = min(D, period_days) / period_days - total_mb += delta_cost + p_baseline * baseline_cost + cost_per_download = cycle_avg_delta + p_needs_baseline * full_avg + total_mb += W * num_downloads * cost_per_download return total_mb @@ -290,7 +335,8 @@ def main(): show.add(p) # --- Print table -------------------------------------------------------- - print(f"--- Schedule Comparison (total compressed egress across {n} checkpoints) ---") + print(f"--- Schedule Comparison (total compressed egress across measurement window) ---") + print(f" ('Total' = sum of all user download events × cost; comparable across schedules)") col_cost = args.cost_per_gb is not None hdr = (f" {'Period':>6} {'Interval':>9} {'Total Egress':>14} {'Avg/Checkpoint':>16}") if col_cost: From 331ba1d23e4653c614df09eee93aaf699bda9829 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 24 Mar 2026 08:06:29 -0700 Subject: [PATCH 11/36] Update script to match with telemetry data better --- tools/DeltaIndexTestTool/analyze.py | 151 +++++++++++----------------- 1 file changed, 58 insertions(+), 93 deletions(-) diff --git a/tools/DeltaIndexTestTool/analyze.py b/tools/DeltaIndexTestTool/analyze.py index 3fdc456b02..50168cb4ab 100644 --- a/tools/DeltaIndexTestTool/analyze.py +++ b/tools/DeltaIndexTestTool/analyze.py @@ -11,17 +11,20 @@ COST MODEL SUMMARY ------------------ -For a refresh period of P checkpoints, the baseline age cycles 0..P-1 in steady state. -For each user type (D days between updates, population weight W): +The distribution W[D] represents the fraction of *download events* from clients +that were D days stale at the time of download (not the fraction of users). +Telemetry naturally produces this view since it counts downloads, not users. +Frequency is therefore already embedded in W[D] — no additional weighting by 1/D. - - Avg delta cost per download = average of DeltaOrig[0..P-1] - (user downloads current delta-from-baseline regardless of personal staleness) - - P(needs new baseline) = min(D, P*interval_days) / (P*interval_days) - (fraction of time the user's last update predates the current baseline) - - Downloads per user over window = window_days / D - (daily users contribute 7x more download events than weekly users) +For a refresh period of P checkpoints (P * interval_days days): -Total egress = sum over buckets of: W * (window/D) * (avg_delta + p_baseline * full_avg) + cycle_avg_delta = average of DeltaOrig[0..P-1] + (expected delta size at a random moment in the cycle) + weighted_p_baseline = sum over D of W[D] * min(D, period_days) / period_days + (expected fraction of downloads that need a new baseline) + cost_per_download = cycle_avg_delta + weighted_p_baseline * full_avg + +Comparing cost_per_download across periods finds the optimal refresh interval. Key approximation: DeltaOrig growth from baseline 0 is used as a proxy for delta growth from any hypothetical baseline (reasonable when repository growth is steady). @@ -49,30 +52,30 @@ PRESETS = { "daily_heavy": { - "description": "Assumption: heavy daily-update user base (CI systems, power users)", + "description": "Assumption (download events): heavy automated/CI usage — many daily downloads", "buckets": [ - {"days": 1, "weight": 0.40}, - {"days": 7, "weight": 0.35}, - {"days": 30, "weight": 0.20}, - {"days": 90, "weight": 0.05}, + {"days": 1, "weight": 0.80}, + {"days": 7, "weight": 0.15}, + {"days": 30, "weight": 0.04}, + {"days": 90, "weight": 0.01}, ], }, "weekly": { - "description": "Assumption: mostly weekly updaters (typical developer)", + "description": "Assumption (download events): typical developer tool — weekly updaters dominate downloads", "buckets": [ - {"days": 1, "weight": 0.10}, + {"days": 1, "weight": 0.30}, {"days": 7, "weight": 0.50}, - {"days": 30, "weight": 0.30}, - {"days": 90, "weight": 0.10}, + {"days": 30, "weight": 0.15}, + {"days": 90, "weight": 0.05}, ], }, "monthly": { - "description": "Assumption: mostly monthly or infrequent updaters", + "description": "Assumption (download events): infrequent updaters dominate downloads", "buckets": [ {"days": 1, "weight": 0.05}, - {"days": 7, "weight": 0.20}, + {"days": 7, "weight": 0.25}, {"days": 30, "weight": 0.45}, - {"days": 90, "weight": 0.30}, + {"days": 90, "weight": 0.25}, ], }, } @@ -143,78 +146,43 @@ def normalize_buckets(buckets): def simulate_schedule(checkpoints, period, buckets, interval_days): """ Simulate a periodic baseline refresh every `period` checkpoints and return - the total expected compressed egress in MB across the measurement window. - - MODEL - ----- - For a given refresh period P (checkpoints), baseline age cycles 0..P-1. - When a user updates at a moment when the baseline is `a` checkpoints old: - - - They always download the current delta: DeltaOrig[a] - - If they are MORE stale than the baseline (their last update was before - the baseline was published), they additionally download the full index. - - The probability that a user of type D (days between updates) needs the - baseline at a random update moment equals the fraction of the baseline - cycle during which the baseline is newer than the user's last update: + the expected compressed egress cost per download event. - p_needs_baseline(D) = min(D, period_days) / period_days + The distribution buckets represent fractions of *download events* by client + staleness (D days since last update). Since frequency is already embedded + in the weights, no additional per-user-type frequency scaling is applied. - This is derived from: the baseline was published `a` days ago; user needs - it if a < D; in steady state `a` is uniform over [0, period_days), so the - probability is min(D, period_days) / period_days. + For a given period P: - The average delta cost across a complete baseline cycle is the mean of - DeltaOrig[0..P-1], since in steady state the baseline is equally likely - to be at any age. + cycle_avg_delta = mean(DeltaOrig[0], ..., DeltaOrig[P-1]) + Expected delta size at a uniformly random moment in + the baseline lifecycle. - FREQUENCY CORRECTION - -------------------- - A user who updates every D days generates window_days/D download events - over the measurement window — not one per checkpoint. The total egress - contribution of each user type is therefore: + weighted_p_baseline = sum over D of: W[D] * min(D, period_days) / period_days + Expected fraction of download events where the client's + index predates the current baseline, requiring a full + baseline download. - W[D] * (window_days / D) * (cycle_avg_delta + p_needs_baseline * full_avg) + cost_per_download = cycle_avg_delta + weighted_p_baseline * full_avg - where W[D] is the population fraction for that update frequency. Summing - across all user types gives the total expected egress for the window. - - This correctly accounts for the fact that daily updaters contribute far - more download events than monthly updaters, even though each individual - download for a daily updater is cheaper (smaller delta, less likely to - need a baseline reset). + Returns cost_per_download (MB). Multiply by total download count for + absolute egress; for schedule comparison the relative values suffice. """ delta_curve = [cp["DeltaOrigCompressedMB"] for cp in checkpoints] n = len(checkpoints) period_days = period * interval_days - window_days = max((n - 1) * interval_days, interval_days) - # Average full index size over the measurement window (baselines are - # published at various sizes as the index grows; use the mean as the - # representative cost of downloading a baseline). full_avg = sum(cp["FullIndexCompressedMB"] for cp in checkpoints) / n - # Average delta cost across a complete baseline cycle. - # DeltaOrig[a] is the measured delta growth from baseline 0; used here as - # a proxy for delta growth from any baseline (the key approximation). cycle_deltas = [delta_curve[min(a, len(delta_curve) - 1)] for a in range(period)] cycle_avg_delta = sum(cycle_deltas) / period - total_mb = 0.0 - for bucket in buckets: - D = bucket["days"] - W = bucket["weight"] - - # Number of download events from this user type over the window. - num_downloads = window_days / D - - # Fraction of those downloads that require a new baseline. - p_needs_baseline = min(D, period_days) / period_days - - cost_per_download = cycle_avg_delta + p_needs_baseline * full_avg - total_mb += W * num_downloads * cost_per_download + weighted_p_baseline = sum( + b["weight"] * min(b["days"], period_days) / period_days + for b in buckets + ) - return total_mb + return cycle_avg_delta + weighted_p_baseline * full_avg def find_crossover(checkpoints, threshold): @@ -314,17 +282,16 @@ def main(): # --- Simulate all periods ----------------------------------------------- results = [] for period in range(1, n + 1): - total_mb = simulate_schedule(checkpoints, period, buckets, interval_days) + cost_per_dl = simulate_schedule(checkpoints, period, buckets, interval_days) results.append({ "period": period, "period_days": period * interval_days, - "total_mb": total_mb, - "avg_mb": total_mb / n, + "total_mb": cost_per_dl, # MB per download event }) - optimal = min(results, key=lambda r: r["total_mb"]) - always_full = results[0] # period == 1 - never_refresh = results[-1] # period == n + optimal = min(results, key=lambda r: r["total_mb"]) + always_full = results[0] # period == 1 + never_refresh = results[-1] # period == n # --- Determine which periods to print in the table ---------------------- # Always show: period 1, optimal, and period n. @@ -335,14 +302,14 @@ def main(): show.add(p) # --- Print table -------------------------------------------------------- - print(f"--- Schedule Comparison (total compressed egress across measurement window) ---") - print(f" ('Total' = sum of all user download events × cost; comparable across schedules)") + print(f"--- Schedule Comparison (expected compressed egress per download event) ---") + print(f" (lower = cheaper per update on average; relative comparison across schedules)") col_cost = args.cost_per_gb is not None - hdr = (f" {'Period':>6} {'Interval':>9} {'Total Egress':>14} {'Avg/Checkpoint':>16}") + hdr = f" {'Period':>6} {'Interval':>9} {'MB/Download':>13}" if col_cost: - hdr += f" {'Total Cost':>12}" + hdr += f" {'$/Download':>12}" print(hdr) - sep = f" {'-'*6} {'-'*9} {'-'*14} {'-'*16}" + (f" {'-'*12}" if col_cost else "") + sep = f" {'-'*6} {'-'*9} {'-'*13}" + (f" {'-'*12}" if col_cost else "") print(sep) for r in sorted(results, key=lambda r: r["period"]): @@ -356,11 +323,10 @@ def main(): elif r["period"] == optimal["period"]: tag = " ← optimal" - line = (f" {r['period']:>6} {fmt_days(r['period_days']):>9} " - f"{fmt_mb(r['total_mb']):>14} {fmt_mb(r['avg_mb']):>16}{tag}") + line = f" {r['period']:>6} {fmt_days(r['period_days']):>9} {r['total_mb']:>11.2f} MB{tag}" if col_cost: cost = r["total_mb"] / 1024 * args.cost_per_gb - line += f" ${cost:>11.2f}" + line += f" ${cost:>11.4f}" print(line) print() @@ -372,15 +338,14 @@ def main(): print(f" Recommendation: refresh baseline every {optimal['period']} checkpoint(s) " f"({fmt_days(optimal['period_days'])})") - print(f" Total egress: {fmt_mb(optimal['total_mb'])} " - f"(avg {fmt_mb(optimal['avg_mb'])} per checkpoint)") + print(f" {optimal['total_mb']:.2f} MB / download event") print(f" vs always-full: {savings_vs_full:+.1f}%") if optimal["period"] < n: print(f" vs never-refresh: {savings_vs_never:+.1f}%") if col_cost: opt_cost = optimal["total_mb"] / 1024 * args.cost_per_gb full_cost = always_full["total_mb"] / 1024 * args.cost_per_gb - print(f" Estimated cost: ${opt_cost:.2f} (vs ${full_cost:.2f} always-full)") + print(f" Cost per download: ${opt_cost:.4f} (vs ${full_cost:.4f} always-full)") print() # --- Chart -------------------------------------------------------------- From 76e3a5160a0b73ba7c41089f4ac41ada6fe0e7aa Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Thu, 23 Apr 2026 18:04:14 -0700 Subject: [PATCH 12/36] Update script for new client concept --- tools/DeltaIndexTestTool/analyze.py | 39 ++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/tools/DeltaIndexTestTool/analyze.py b/tools/DeltaIndexTestTool/analyze.py index 50168cb4ab..f94f4177b7 100644 --- a/tools/DeltaIndexTestTool/analyze.py +++ b/tools/DeltaIndexTestTool/analyze.py @@ -22,6 +22,7 @@ (expected delta size at a random moment in the cycle) weighted_p_baseline = sum over D of W[D] * min(D, period_days) / period_days (expected fraction of downloads that need a new baseline) + For "new_client" buckets: p_needs_baseline = 1.0 always. cost_per_download = cycle_avg_delta + weighted_p_baseline * full_avg Comparing cost_per_download across periods finds the optimal refresh interval. @@ -29,6 +30,26 @@ Key approximation: DeltaOrig growth from baseline 0 is used as a proxy for delta growth from any hypothetical baseline (reasonable when repository growth is steady). +DISTRIBUTION FORMAT +------------------- +Buckets can be either: + { "days": N, "weight": W } -- clients N days stale at update time + { "new_client": true, "weight": W } -- net-new clients (no prior index; always + pay full baseline cost) + +Built-in presets: daily_heavy, weekly, monthly + +Telemetry-derived JSON example: + { + "description": "Telemetry-derived YYYY-MM-DD", + "buckets": [ + { "days": 1, "weight": 0.30 }, + { "days": 7, "weight": 0.50 }, + { "days": 30, "weight": 0.15 }, + { "new_client": true, "weight": 0.05 } + ] + } + Usage: python analyze.py --csv results.csv --distribution weekly python analyze.py --csv results.csv --distribution distribution.json @@ -135,12 +156,24 @@ def compute_interval_days(checkpoints): def normalize_buckets(buckets): - """Return a copy of buckets with weights normalized to sum to 1.0.""" + """Return a copy of buckets with weights normalized to sum to 1.0. + + Supports both regular staleness buckets {"days": N, "weight": W} and + net-new client buckets {"new_client": true, "weight": W}. + """ total = sum(b["weight"] for b in buckets) if abs(total - 1.0) > 0.01: print(f"Warning: distribution weights sum to {total:.3f}, normalizing to 1.0", file=sys.stderr) - return [{"days": b["days"], "weight": b["weight"] / total} for b in buckets] + result = [] + for b in buckets: + normalized = {"weight": b["weight"] / total} + if b.get("new_client"): + normalized["new_client"] = True + else: + normalized["days"] = b["days"] + result.append(normalized) + return result def simulate_schedule(checkpoints, period, buckets, interval_days): @@ -178,7 +211,7 @@ def simulate_schedule(checkpoints, period, buckets, interval_days): cycle_avg_delta = sum(cycle_deltas) / period weighted_p_baseline = sum( - b["weight"] * min(b["days"], period_days) / period_days + b["weight"] * (1.0 if b.get("new_client") else min(b["days"], period_days) / period_days) for b in buckets ) From 0ba3b2542b8d4056268a636d49a02892412aeaa1 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Tue, 1 Sep 2026 23:50:47 -0700 Subject: [PATCH 13/36] Improve script for telemetry --- tools/DeltaIndexTestTool/analyze.py | 162 +- .../DeltaIndexTestTool/baseline-analysis.png | Bin 0 -> 136103 bytes tools/DeltaIndexTestTool/dist-observed.json | 7205 ++++++++++++++++ tools/DeltaIndexTestTool/dist-tail-10.json | 7301 +++++++++++++++++ tools/DeltaIndexTestTool/dist-tail-20.json | 7301 +++++++++++++++++ tools/DeltaIndexTestTool/dist-tail-30.json | 7301 +++++++++++++++++ tools/DeltaIndexTestTool/make_distribution.py | 284 + 7 files changed, 29498 insertions(+), 56 deletions(-) create mode 100644 tools/DeltaIndexTestTool/baseline-analysis.png create mode 100644 tools/DeltaIndexTestTool/dist-observed.json create mode 100644 tools/DeltaIndexTestTool/dist-tail-10.json create mode 100644 tools/DeltaIndexTestTool/dist-tail-20.json create mode 100644 tools/DeltaIndexTestTool/dist-tail-30.json create mode 100644 tools/DeltaIndexTestTool/make_distribution.py diff --git a/tools/DeltaIndexTestTool/analyze.py b/tools/DeltaIndexTestTool/analyze.py index f94f4177b7..d39ce06b1c 100644 --- a/tools/DeltaIndexTestTool/analyze.py +++ b/tools/DeltaIndexTestTool/analyze.py @@ -4,11 +4,13 @@ """ analyze.py - Baseline refresh timing optimizer for winget delta indexes. -Reads a results.csv produced by DeltaIndexTestTool and models the total compressed +Reads a results.csv produced by DeltaIndexTestTool and models the relative compressed egress across multiple user updates under different baseline refresh schedules. -Sweeps all candidate refresh periods and recommends the one that minimizes total +Sweeps all candidate refresh periods and recommends the one that minimizes the expected compressed egress given an assumed user staleness distribution. +All output is relative (percentages); absolute egress volume is never required. + COST MODEL SUMMARY ------------------ The distribution W[D] represents the fraction of *download events* from clients @@ -23,9 +25,23 @@ weighted_p_baseline = sum over D of W[D] * min(D, period_days) / period_days (expected fraction of downloads that need a new baseline) For "new_client" buckets: p_needs_baseline = 1.0 always. - cost_per_download = cycle_avg_delta + weighted_p_baseline * full_avg + cost_per_download = cycle_avg_delta + weighted_p_baseline * baseline_size + +The status quo (no deltas at all) costs `baseline_size` per download, so the +predicted traffic reduction for a schedule is simply: + + reduction = 1 - cost_per_download / baseline_size + +BASELINE SIZE +------------- +`baseline_size` is the compressed size of the index a client downloads when it +must take a fresh baseline. By default it is taken from the *last* checkpoint in +the CSV (the most current measurement). Because the tool-built index may not match +what production actually serves, supply the real value with --baseline-mb. -Comparing cost_per_download across periods finds the optimal refresh interval. +Delta sizes are NOT scaled along with --baseline-mb: delta growth is driven by the +repository change rate, which holds relatively steady and is measured directly by +the tool. Only the baseline-download side of the model responds to --baseline-mb. Key approximation: DeltaOrig growth from baseline 0 is used as a proxy for delta growth from any hypothetical baseline (reasonable when repository growth is steady). @@ -53,13 +69,14 @@ Usage: python analyze.py --csv results.csv --distribution weekly python analyze.py --csv results.csv --distribution distribution.json - python analyze.py --csv results.csv --distribution weekly --egress-cost-per-gb 0.087 --output-chart chart.png + python analyze.py --csv results.csv --distribution weekly --baseline-mb 12.4 --output-chart chart.png """ import argparse import csv import json import sys +import textwrap from datetime import datetime from pathlib import Path @@ -176,7 +193,7 @@ def normalize_buckets(buckets): return result -def simulate_schedule(checkpoints, period, buckets, interval_days): +def simulate_schedule(checkpoints, period, buckets, interval_days, baseline_size): """ Simulate a periodic baseline refresh every `period` checkpoints and return the expected compressed egress cost per download event. @@ -196,17 +213,18 @@ def simulate_schedule(checkpoints, period, buckets, interval_days): index predates the current baseline, requiring a full baseline download. - cost_per_download = cycle_avg_delta + weighted_p_baseline * full_avg + cost_per_download = cycle_avg_delta + weighted_p_baseline * baseline_size - Returns cost_per_download (MB). Multiply by total download count for - absolute egress; for schedule comparison the relative values suffice. + `baseline_size` is the compressed MB a client pays for a fresh baseline; see + the module docstring for how it is chosen. Delta sizes come from measured + repository change rate and are deliberately independent of it. + + Returns cost_per_download (MB). Only ratios between schedules (and against + `baseline_size`, the status quo) are meaningful. """ delta_curve = [cp["DeltaOrigCompressedMB"] for cp in checkpoints] - n = len(checkpoints) period_days = period * interval_days - full_avg = sum(cp["FullIndexCompressedMB"] for cp in checkpoints) / n - cycle_deltas = [delta_curve[min(a, len(delta_curve) - 1)] for a in range(period)] cycle_avg_delta = sum(cycle_deltas) / period @@ -215,17 +233,18 @@ def simulate_schedule(checkpoints, period, buckets, interval_days): for b in buckets ) - return cycle_avg_delta + weighted_p_baseline * full_avg + return cycle_avg_delta + weighted_p_baseline * baseline_size -def find_crossover(checkpoints, threshold): +def find_crossover(checkpoints, threshold, baseline_size): """ Return the index of the first checkpoint where - DeltaOrigCompressedMB / FullIndexCompressedMB >= threshold, or None. + DeltaOrigCompressedMB / baseline_size >= threshold, or None. """ + if baseline_size <= 0: + return None for i, cp in enumerate(checkpoints): - full = cp["FullIndexCompressedMB"] - if full > 0 and cp["DeltaOrigCompressedMB"] / full >= threshold: + if cp["DeltaOrigCompressedMB"] / baseline_size >= threshold: return i return None @@ -253,6 +272,14 @@ def fmt_days(days): # --------------------------------------------------------------------------- def main(): + # Windows consoles often default to cp1252, which cannot encode the arrows and + # dashes used below. Prefer UTF-8, and degrade to replacement chars if unavailable. + for stream in (sys.stdout, sys.stderr): + try: + stream.reconfigure(encoding="utf-8", errors="replace") + except (AttributeError, ValueError): + pass + parser = argparse.ArgumentParser( description="Optimize baseline refresh timing for winget delta indexes.", formatter_class=argparse.RawDescriptionHelpFormatter, @@ -263,8 +290,10 @@ def main(): parser.add_argument("--distribution", required=True, help="Staleness distribution: preset name or path to JSON file. " f"Presets: {', '.join(PRESETS)}") - parser.add_argument("--egress-cost-per-gb", type=float, default=None, dest="cost_per_gb", - help="Optional egress cost per GB. If omitted, output is in bytes only.") + parser.add_argument("--baseline-mb", type=float, default=None, dest="baseline_mb", + help="Compressed size (MB) of the baseline index clients actually download. " + "Defaults to the last checkpoint's FullIndexCompressedMB from the CSV. " + "Delta sizes are not scaled by this value.") parser.add_argument("--output-chart", default=None, dest="output_chart", help="Optional path to save a chart image (e.g. chart.png). Requires matplotlib.") args = parser.parse_args() @@ -282,6 +311,15 @@ def main(): interval_days = compute_interval_days(checkpoints) n = len(checkpoints) + # The size a client pays for a fresh baseline. The CSV's last checkpoint is + # the most current measurement, but production may serve a different index; + # --baseline-mb lets telemetry-observed reality drive the model instead. + csv_baseline_size = checkpoints[-1]["FullIndexCompressedMB"] + baseline_size = args.baseline_mb if args.baseline_mb is not None else csv_baseline_size + if baseline_size <= 0: + print("Error: baseline size is zero; supply --baseline-mb.", file=sys.stderr) + sys.exit(1) + # --- Header ------------------------------------------------------------- print(f"\n{'='*68}") print(f" Baseline Timing Analysis") @@ -292,30 +330,33 @@ def main(): print(f" Avg interval: {interval_days:.1f} days " f"(total span: {(checkpoints[-1]['Date'] - checkpoints[0]['Date']).days} days)") print(f" Distribution: {description}") - if args.cost_per_gb is not None: - print(f" Egress cost: ${args.cost_per_gb:.4f}/GB") + if args.baseline_mb is not None: + print(f" Baseline: {fmt_mb(baseline_size)} (supplied; " + f"CSV measured {fmt_mb(csv_baseline_size)})") + else: + print(f" Baseline: {fmt_mb(baseline_size)} (last checkpoint in CSV)") print() # --- Delta growth crossovers ------------------------------------------- - c50 = find_crossover(checkpoints, 0.50) - c100 = find_crossover(checkpoints, 1.00) + c50 = find_crossover(checkpoints, 0.50, baseline_size) + c100 = find_crossover(checkpoints, 1.00, baseline_size) print(" Delta (compressed) growth from baseline:") if c50 is not None: - print(f" Exceeds 50% of full index at checkpoint {c50:>3} " + print(f" Exceeds 50% of baseline at checkpoint {c50:>3} " f"({fmt_days(c50 * interval_days)})") else: - print(" Never exceeds 50% of full index within measured period") + print(" Never exceeds 50% of baseline within measured period") if c100 is not None: - print(f" Exceeds 100% of full index at checkpoint {c100:>3} " + print(f" Exceeds 100% of baseline at checkpoint {c100:>3} " f"({fmt_days(c100 * interval_days)})") else: - print(" Never exceeds 100% of full index within measured period") + print(" Never exceeds 100% of baseline within measured period") print() # --- Simulate all periods ----------------------------------------------- results = [] for period in range(1, n + 1): - cost_per_dl = simulate_schedule(checkpoints, period, buckets, interval_days) + cost_per_dl = simulate_schedule(checkpoints, period, buckets, interval_days, baseline_size) results.append({ "period": period, "period_days": period * interval_days, @@ -323,9 +364,11 @@ def main(): }) optimal = min(results, key=lambda r: r["total_mb"]) - always_full = results[0] # period == 1 never_refresh = results[-1] # period == n + # Status quo: no deltas at all, every download fetches the whole index. + status_quo_mb = baseline_size + # --- Determine which periods to print in the table ---------------------- # Always show: period 1, optimal, and period n. # Also show a sample of ~15-20 evenly-spaced periods in between. @@ -336,59 +379,59 @@ def main(): # --- Print table -------------------------------------------------------- print(f"--- Schedule Comparison (expected compressed egress per download event) ---") - print(f" (lower = cheaper per update on average; relative comparison across schedules)") - col_cost = args.cost_per_gb is not None - hdr = f" {'Period':>6} {'Interval':>9} {'MB/Download':>13}" - if col_cost: - hdr += f" {'$/Download':>12}" - print(hdr) - sep = f" {'-'*6} {'-'*9} {'-'*13}" + (f" {'-'*12}" if col_cost else "") - print(sep) + print(f" (lower = cheaper per update on average; % is reduction vs. today's " + f"full-index-every-time behavior)") + print(f" {'Period':>6} {'Interval':>9} {'MB/Download':>13} {'vs Status Quo':>14}") + print(f" {'-'*6} {'-'*9} {'-'*13} {'-'*14}") + + print(f" {'-':>6} {'-':>9} {status_quo_mb:>11.2f} MB " + f"{'baseline':>14} (status quo: full index every download)") for r in sorted(results, key=lambda r: r["period"]): if r["period"] not in show: continue tag = "" - if r["period"] == 1: - tag = " (always full)" - elif r["period"] == n: + if r["period"] == n: tag = " (never refresh)" elif r["period"] == optimal["period"]: tag = " ← optimal" - line = f" {r['period']:>6} {fmt_days(r['period_days']):>9} {r['total_mb']:>11.2f} MB{tag}" - if col_cost: - cost = r["total_mb"] / 1024 * args.cost_per_gb - line += f" ${cost:>11.4f}" - print(line) + reduction = 100.0 * (1.0 - r["total_mb"] / status_quo_mb) + print(f" {r['period']:>6} {fmt_days(r['period_days']):>9} " + f"{r['total_mb']:>11.2f} MB {reduction:>13.1f}%{tag}") print() # --- Recommendation summary --------------------------------------------- - savings_vs_full = 100.0 * (1.0 - optimal["total_mb"] / always_full["total_mb"]) + reduction_vs_status_quo = 100.0 * (1.0 - optimal["total_mb"] / status_quo_mb) savings_vs_never = (100.0 * (1.0 - optimal["total_mb"] / never_refresh["total_mb"]) if never_refresh["total_mb"] > 0 else 0.0) print(f" Recommendation: refresh baseline every {optimal['period']} checkpoint(s) " f"({fmt_days(optimal['period_days'])})") print(f" {optimal['total_mb']:.2f} MB / download event") - print(f" vs always-full: {savings_vs_full:+.1f}%") + print(f" Predicted outbound traffic reduction: {reduction_vs_status_quo:.1f}%") if optimal["period"] < n: print(f" vs never-refresh: {savings_vs_never:+.1f}%") - if col_cost: - opt_cost = optimal["total_mb"] / 1024 * args.cost_per_gb - full_cost = always_full["total_mb"] / 1024 * args.cost_per_gb - print(f" Cost per download: ${opt_cost:.4f} (vs ${full_cost:.4f} always-full)") print() # --- Chart -------------------------------------------------------------- if args.output_chart: _write_chart(args.output_chart, checkpoints, optimal, interval_days, - description, always_full, never_refresh) + description, baseline_size) + + +def _mb_precision(ticks): + """Decimal places needed so adjacent axis ticks render as distinct labels.""" + spacing = min((abs(b - a) for a, b in zip(ticks, ticks[1:])), default=1.0) + if spacing >= 1.0: + return 0 + if spacing >= 0.1: + return 1 + return 2 -def _write_chart(path, checkpoints, optimal, interval_days, description, - always_full, never_refresh): +def _write_chart(path, checkpoints, optimal, interval_days, description, baseline_size): try: import matplotlib matplotlib.use("Agg") @@ -408,6 +451,8 @@ def _write_chart(path, checkpoints, optimal, interval_days, description, ax.plot(dates, full_vals, label="Full index (compressed)", color="#C0392B", linewidth=2) ax.plot(dates, delta_vals, label="Delta from baseline (compressed)", color="#27AE60", linewidth=2) + ax.axhline(y=baseline_size, color="#7F8C8D", linestyle=":", linewidth=1.5, + label=f"Baseline download size ({baseline_size:.1f} MB)") # Vertical lines at recommended baseline refresh points opt_period = optimal["period"] @@ -427,9 +472,14 @@ def _write_chart(path, checkpoints, optimal, interval_days, description, ax.set_xlabel("Date") ax.set_ylabel("Compressed Size (MB)") - ax.set_title(f"Delta Index Baseline Timing Analysis\nDistribution: {description}") + ax.set_title("Delta Index Baseline Timing Analysis\n" + + "\n".join(textwrap.wrap(f"Distribution: {description}", 110)), + fontsize=11) ax.legend(loc="upper left") - ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f"{x:.0f} MB")) + # Whole-MB labels collapse into duplicates when the plotted range is only a few MB, + # so pick the precision from the actual tick spacing. + ax.yaxis.set_major_formatter(ticker.FuncFormatter( + lambda v, _: f"{v:.{_mb_precision(ax.get_yticks())}f} MB")) fig.autofmt_xdate() plt.tight_layout() plt.savefig(path, dpi=150) diff --git a/tools/DeltaIndexTestTool/baseline-analysis.png b/tools/DeltaIndexTestTool/baseline-analysis.png new file mode 100644 index 0000000000000000000000000000000000000000..262156c7a9fdddcd1ad3f258769f2e010a40ffc7 GIT binary patch literal 136103 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 literal 0 HcmV?d00001 diff --git a/tools/DeltaIndexTestTool/dist-observed.json b/tools/DeltaIndexTestTool/dist-observed.json new file mode 100644 index 0000000000..8cb9f08900 --- /dev/null +++ b/tools/DeltaIndexTestTool/dist-observed.json @@ -0,0 +1,7205 @@ +{ + "description": "Telemetry-derived from C:\\Users\\johnmcp\\Downloads\\export.csv: 549,093 download events, 2.21% net-new clients, observed ages 0-18.0 days", + "buckets": [ + { + "days": 0.0, + "weight": 5.099318330410331e-05 + }, + { + "days": 0.000694, + "weight": 0.0005408919800470958 + }, + { + "days": 0.028472, + "weight": 0.002178137401132413 + }, + { + "days": 0.042361, + "weight": 5.827792377611807e-05 + }, + { + "days": 0.043056, + "weight": 8.195333031016604e-05 + }, + { + "days": 0.047222, + "weight": 0.008171657624482557 + }, + { + "days": 0.049306, + "weight": 5.099318330410331e-05 + }, + { + "days": 0.050694, + "weight": 0.0035476686098711878 + }, + { + "days": 0.051389, + "weight": 0.00886006559908795 + }, + { + "days": 0.052083, + "weight": 3.6423702360073794e-05 + }, + { + "days": 0.054167, + "weight": 0.005119351366708371 + }, + { + "days": 0.054861, + "weight": 0.008016856889452242 + }, + { + "days": 0.055556, + "weight": 0.006589047756937349 + }, + { + "days": 0.056944, + "weight": 0.007004277963842191 + }, + { + "days": 0.057639, + "weight": 0.009665029421245582 + }, + { + "days": 0.058333, + "weight": 0.00428889095289869 + }, + { + "days": 0.059028, + "weight": 0.04424933481213565 + }, + { + "days": 0.059722, + "weight": 0.02019694295866092 + }, + { + "days": 0.060417, + "weight": 0.008477616724307175 + }, + { + "days": 0.061111, + "weight": 0.018180891033030836 + }, + { + "days": 0.061806, + "weight": 0.020333531842511195 + }, + { + "days": 0.0625, + "weight": 0.0037935286008016857 + }, + { + "days": 0.063194, + "weight": 0.014030410149100425 + }, + { + "days": 0.063889, + "weight": 0.01562758949758966 + }, + { + "days": 0.064583, + "weight": 0.003698826974665494 + }, + { + "days": 0.065278, + "weight": 0.008226293178022667 + }, + { + "days": 0.065972, + "weight": 0.010284232361366837 + }, + { + "days": 0.066667, + "weight": 0.016946127523024334 + }, + { + "days": 0.067361, + "weight": 0.0046877304937414975 + }, + { + "days": 0.069444, + "weight": 0.006570835905757313 + }, + { + "days": 0.070833, + "weight": 0.004013892000080132 + }, + { + "days": 0.072222, + "weight": 0.004247003695184604 + }, + { + "days": 0.072917, + "weight": 0.003726144751435549 + }, + { + "days": 0.074306, + "weight": 0.012977765150894292 + }, + { + "days": 0.076389, + "weight": 6.556266424813284e-05 + }, + { + "days": 0.078472, + "weight": 0.005257761435676652 + }, + { + "days": 0.084028, + "weight": 0.0034893906860950694 + }, + { + "days": 0.084722, + "weight": 0.005062894628050258 + }, + { + "days": 0.085417, + "weight": 0.005155775069068446 + }, + { + "days": 0.0875, + "weight": 0.00024039643557648705 + }, + { + "days": 0.088889, + "weight": 0.0024258185771809148 + }, + { + "days": 0.102083, + "weight": 0.0013039685444906418 + }, + { + "days": 0.109722, + "weight": 0.0012493329909505312 + }, + { + "days": 0.110417, + "weight": 0.0020124095553940772 + }, + { + "days": 0.113194, + "weight": 0.00502464974057218 + }, + { + "days": 0.114583, + "weight": 0.0016135700145512692 + }, + { + "days": 0.115278, + "weight": 7.284740472014759e-05 + }, + { + "days": 0.115972, + "weight": 4.188725771408486e-05 + }, + { + "days": 0.116667, + "weight": 0.0016354242359673133 + }, + { + "days": 0.117361, + "weight": 0.003261742546344608 + }, + { + "days": 0.118056, + "weight": 0.0033546229873627964 + }, + { + "days": 0.11875, + "weight": 0.002159925549952376 + }, + { + "days": 0.119444, + "weight": 0.0017009869002154463 + }, + { + "days": 0.120139, + "weight": 0.002194528067194446 + }, + { + "days": 0.120833, + "weight": 0.001914065559021878 + }, + { + "days": 0.121528, + "weight": 0.0019413833357919332 + }, + { + "days": 0.122222, + "weight": 0.001766549564463579 + }, + { + "days": 0.122917, + "weight": 0.0014132396515708632 + }, + { + "days": 0.123611, + "weight": 0.0006774808638973725 + }, + { + "days": 0.124306, + "weight": 0.002003303629804059 + }, + { + "days": 0.125, + "weight": 0.000744864713263509 + }, + { + "days": 0.125694, + "weight": 0.004664055087207449 + }, + { + "days": 0.126389, + "weight": 0.0008595993756977415 + }, + { + "days": 0.127083, + "weight": 0.0027973403412536674 + }, + { + "days": 0.127778, + "weight": 0.0028191945626697117 + }, + { + "days": 0.130556, + "weight": 0.00030960147006062726 + }, + { + "days": 0.13125, + "weight": 0.0014660540199929702 + }, + { + "days": 0.131944, + "weight": 0.0007831096007415866 + }, + { + "days": 0.132639, + "weight": 0.0015134048330610662 + }, + { + "days": 0.133333, + "weight": 0.003010419000060099 + }, + { + "days": 0.134028, + "weight": 0.0023384016915167375 + }, + { + "days": 0.134722, + "weight": 0.0007211893067294612 + }, + { + "days": 0.136111, + "weight": 0.0014879082414090145 + }, + { + "days": 0.1375, + "weight": 0.0006210241252392582 + }, + { + "days": 0.138194, + "weight": 0.0005481767205191106 + }, + { + "days": 0.14375, + "weight": 0.00043526324320288185 + }, + { + "days": 0.144444, + "weight": 0.0012839355081926012 + }, + { + "days": 0.147917, + "weight": 0.001180127956466391 + }, + { + "days": 0.148611, + "weight": 0.0002658930272285387 + }, + { + "days": 0.149306, + "weight": 0.0009998306297840257 + }, + { + "days": 0.150694, + "weight": 0.001367710023620771 + }, + { + "days": 0.161111, + "weight": 0.0007721824900335645 + }, + { + "days": 0.164583, + "weight": 3.8244887478077486e-05 + }, + { + "days": 0.168056, + "weight": 0.0007612553793255423 + }, + { + "days": 0.169444, + "weight": 0.0017100928258054647 + }, + { + "days": 0.172222, + "weight": 0.0007612553793255423 + }, + { + "days": 0.172917, + "weight": 6.738384936613652e-05 + }, + { + "days": 0.173611, + "weight": 0.0011309559582802914 + }, + { + "days": 0.175, + "weight": 0.000575494497289166 + }, + { + "days": 0.175694, + "weight": 0.0014004913557448375 + }, + { + "days": 0.176389, + "weight": 0.0006501630871273172 + }, + { + "days": 0.177083, + "weight": 0.0011182076624542655 + }, + { + "days": 0.177778, + "weight": 0.0020615815535801768 + }, + { + "days": 0.178472, + "weight": 0.0006720173085433616 + }, + { + "days": 0.179167, + "weight": 0.002440388058124944 + }, + { + "days": 0.179861, + "weight": 0.0011236712178082765 + }, + { + "days": 0.18125, + "weight": 0.002658930272285387 + }, + { + "days": 0.181944, + "weight": 0.0014460209836949297 + }, + { + "days": 0.182639, + "weight": 0.0005846004228791844 + }, + { + "days": 0.183333, + "weight": 0.0005827792377611807 + }, + { + "days": 0.184028, + "weight": 0.0011054593666282397 + }, + { + "days": 0.184722, + "weight": 0.001502477722353044 + }, + { + "days": 0.185417, + "weight": 0.000768540119797557 + }, + { + "days": 0.186111, + "weight": 0.00275909545377559 + }, + { + "days": 0.186806, + "weight": 0.0003314556914766715 + }, + { + "days": 0.1875, + "weight": 0.000475329315798963 + }, + { + "days": 0.188889, + "weight": 0.0008741688566417711 + }, + { + "days": 0.190972, + "weight": 0.0005354284246930847 + }, + { + "days": 0.192361, + "weight": 0.0015625768312471657 + }, + { + "days": 0.194444, + "weight": 0.0007066198257854316 + }, + { + "days": 0.195139, + "weight": 0.003063233368482206 + }, + { + "days": 0.196528, + "weight": 0.0012639024718945607 + }, + { + "days": 0.197222, + "weight": 0.0004425479836748966 + }, + { + "days": 0.197917, + "weight": 0.000287747248644583 + }, + { + "days": 0.2, + "weight": 0.0003951971706068007 + }, + { + "days": 0.202083, + "weight": 0.0016117488294332654 + }, + { + "days": 0.204167, + "weight": 0.0006756596787793689 + }, + { + "days": 0.208333, + "weight": 0.0007248316769654685 + }, + { + "days": 0.210417, + "weight": 0.0008832747822317895 + }, + { + "days": 0.211111, + "weight": 0.0005408919800470958 + }, + { + "days": 0.2125, + "weight": 0.000657447827599332 + }, + { + "days": 0.218056, + "weight": 0.00022582695463245752 + }, + { + "days": 0.222917, + "weight": 0.00036423702360073796 + }, + { + "days": 0.227083, + "weight": 0.0003496675426567084 + }, + { + "days": 0.229167, + "weight": 0.00011837703267023983 + }, + { + "days": 0.23125, + "weight": 0.0008013214519216235 + }, + { + "days": 0.232639, + "weight": 0.00013294651361426936 + }, + { + "days": 0.233333, + "weight": 0.00010927110708022139 + }, + { + "days": 0.234722, + "weight": 0.00012019821778824352 + }, + { + "days": 0.235417, + "weight": 0.00011655584755223614 + }, + { + "days": 0.236111, + "weight": 0.00030778028494262354 + }, + { + "days": 0.2375, + "weight": 0.0003041379147066162 + }, + { + "days": 0.238194, + "weight": 0.0001256617731422546 + }, + { + "days": 0.238889, + "weight": 0.000475329315798963 + }, + { + "days": 0.239583, + "weight": 0.0002531447314025129 + }, + { + "days": 0.240278, + "weight": 0.00046440220509094086 + }, + { + "days": 0.240972, + "weight": 0.0004935411669789999 + }, + { + "days": 0.242361, + "weight": 0.0004061242813148228 + }, + { + "days": 0.243056, + "weight": 0.0009014866334118264 + }, + { + "days": 0.24375, + "weight": 0.0006210241252392582 + }, + { + "days": 0.244444, + "weight": 0.00012748295826025828 + }, + { + "days": 0.245833, + "weight": 0.0002695353974645461 + }, + { + "days": 0.247917, + "weight": 0.00034056161706669 + }, + { + "days": 0.248611, + "weight": 9.105925590018449e-05 + }, + { + "days": 0.249306, + "weight": 0.00027864132305456454 + }, + { + "days": 0.251389, + "weight": 0.0001420524392042878 + }, + { + "days": 0.252083, + "weight": 0.0013185380254346713 + }, + { + "days": 0.252778, + "weight": 0.00030778028494262354 + }, + { + "days": 0.253472, + "weight": 0.0005208589437490553 + }, + { + "days": 0.254167, + "weight": 0.00026225065699253135 + }, + { + "days": 0.254861, + "weight": 0.0001238405880242509 + }, + { + "days": 0.255556, + "weight": 0.00014933717967630255 + }, + { + "days": 0.25625, + "weight": 0.00013476769873227304 + }, + { + "days": 0.258333, + "weight": 0.00029138961888059035 + }, + { + "days": 0.259028, + "weight": 9.834399637219924e-05 + }, + { + "days": 0.259722, + "weight": 8.74168856641771e-05 + }, + { + "days": 0.261111, + "weight": 0.0002822836932905719 + }, + { + "days": 0.265278, + "weight": 0.00016754903085633945 + }, + { + "days": 0.268056, + "weight": 0.00022218458439645015 + }, + { + "days": 0.271528, + "weight": 0.0004990047223330109 + }, + { + "days": 0.274306, + "weight": 0.00011837703267023983 + }, + { + "days": 0.275, + "weight": 0.00012748295826025828 + }, + { + "days": 0.276389, + "weight": 0.0001438736243222915 + }, + { + "days": 0.279861, + "weight": 0.0002531447314025129 + }, + { + "days": 0.281944, + "weight": 0.00017847614156436158 + }, + { + "days": 0.284028, + "weight": 4.7350813068095936e-05 + }, + { + "days": 0.286806, + "weight": 0.00014569480944029518 + }, + { + "days": 0.288194, + "weight": 9.834399637219924e-05 + }, + { + "days": 0.290278, + "weight": 0.0002658930272285387 + }, + { + "days": 0.292361, + "weight": 0.00012201940290624721 + }, + { + "days": 0.293056, + "weight": 0.000158443105266321 + }, + { + "days": 0.295139, + "weight": 0.00014569480944029518 + }, + { + "days": 0.297222, + "weight": 8.74168856641771e-05 + }, + { + "days": 0.297917, + "weight": 0.0004389056134388892 + }, + { + "days": 0.299306, + "weight": 0.0004953623520970036 + }, + { + "days": 0.3, + "weight": 0.0002695353974645461 + }, + { + "days": 0.300694, + "weight": 0.0003715217640727527 + }, + { + "days": 0.301389, + "weight": 0.00022946932486846491 + }, + { + "days": 0.303472, + "weight": 8.195333031016604e-05 + }, + { + "days": 0.304167, + "weight": 0.0003842700598987785 + }, + { + "days": 0.305556, + "weight": 0.0003951971706068007 + }, + { + "days": 0.30625, + "weight": 0.00022036339927844646 + }, + { + "days": 0.306944, + "weight": 0.0001748337713283542 + }, + { + "days": 0.308333, + "weight": 0.0003023167295886125 + }, + { + "days": 0.309722, + "weight": 0.001273008397484579 + }, + { + "days": 0.310417, + "weight": 0.0001438736243222915 + }, + { + "days": 0.311111, + "weight": 0.00020215154809840956 + }, + { + "days": 0.311806, + "weight": 0.00048443524138898147 + }, + { + "days": 0.3125, + "weight": 0.0003059590998246199 + }, + { + "days": 0.313194, + "weight": 0.00044619035391090396 + }, + { + "days": 0.313889, + "weight": 0.0001384100689682804 + }, + { + "days": 0.314583, + "weight": 0.00018758206715438003 + }, + { + "days": 0.315972, + "weight": 0.0001402312540862841 + }, + { + "days": 0.316667, + "weight": 0.00010927110708022139 + }, + { + "days": 0.317361, + "weight": 0.00013476769873227304 + }, + { + "days": 0.31875, + "weight": 5.2814368422107004e-05 + }, + { + "days": 0.320139, + "weight": 0.00037516413430876007 + }, + { + "days": 0.320833, + "weight": 0.00010016518149020294 + }, + { + "days": 0.322222, + "weight": 0.00020579391833441693 + }, + { + "days": 0.323611, + "weight": 5.4635553540110695e-05 + }, + { + "days": 0.326389, + "weight": 0.00018940325227238374 + }, + { + "days": 0.327778, + "weight": 0.00015297954991230994 + }, + { + "days": 0.33125, + "weight": 0.00022400576951445383 + }, + { + "days": 0.332639, + "weight": 0.007545169943889286 + }, + { + "days": 0.335417, + "weight": 0.00020943628857042433 + }, + { + "days": 0.338194, + "weight": 0.00010562873684421401 + }, + { + "days": 0.340278, + "weight": 0.00017301258621035053 + }, + { + "days": 0.340972, + "weight": 0.0003769853194267638 + }, + { + "days": 0.343056, + "weight": 0.00014569480944029518 + }, + { + "days": 0.34375, + "weight": 0.00018393969691837266 + }, + { + "days": 0.345139, + "weight": 3.46025172420701e-05 + }, + { + "days": 0.345833, + "weight": 0.0001748337713283542 + }, + { + "days": 0.349306, + "weight": 0.00011837703267023983 + }, + { + "days": 0.353472, + "weight": 0.00014933717967630255 + }, + { + "days": 0.354861, + "weight": 0.00013658888385027673 + }, + { + "days": 0.356944, + "weight": 8.559570054617342e-05 + }, + { + "days": 0.357639, + "weight": 0.00017119140109234684 + }, + { + "days": 0.358333, + "weight": 7.466858983815127e-05 + }, + { + "days": 0.359028, + "weight": 0.0006701961234253578 + }, + { + "days": 0.359722, + "weight": 0.00027499895281855717 + }, + { + "days": 0.360417, + "weight": 0.00021854221416044278 + }, + { + "days": 0.361111, + "weight": 0.00012748295826025828 + }, + { + "days": 0.361806, + "weight": 5.4635553540110695e-05 + }, + { + "days": 0.3625, + "weight": 0.0003423828021846937 + }, + { + "days": 0.363194, + "weight": 0.00025678710163852027 + }, + { + "days": 0.363889, + "weight": 0.00010198636660820662 + }, + { + "days": 0.365972, + "weight": 0.00021489984392443538 + }, + { + "days": 0.366667, + "weight": 0.00010380755172621031 + }, + { + "days": 0.367361, + "weight": 7.648977495615497e-05 + }, + { + "days": 0.36875, + "weight": 0.00012748295826025828 + }, + { + "days": 0.369444, + "weight": 0.000158443105266321 + }, + { + "days": 0.370139, + "weight": 0.0003023167295886125 + }, + { + "days": 0.371528, + "weight": 0.0004735081306809593 + }, + { + "days": 0.372222, + "weight": 0.0001802973266823653 + }, + { + "days": 0.372917, + "weight": 0.0002513235462845092 + }, + { + "days": 0.373611, + "weight": 0.00022400576951445383 + }, + { + "days": 0.375, + "weight": 0.0018321122287117118 + }, + { + "days": 0.376389, + "weight": 0.0004316208729668745 + }, + { + "days": 0.377083, + "weight": 0.00017301258621035053 + }, + { + "days": 0.377778, + "weight": 0.00020397273321641325 + }, + { + "days": 0.378472, + "weight": 0.0001802973266823653 + }, + { + "days": 0.379861, + "weight": 0.00014569480944029518 + }, + { + "days": 0.38125, + "weight": 0.00015297954991230994 + }, + { + "days": 0.3875, + "weight": 0.0001438736243222915 + }, + { + "days": 0.388889, + "weight": 5.645673865811438e-05 + }, + { + "days": 0.390278, + "weight": 0.00012930414337826196 + }, + { + "days": 0.392361, + "weight": 0.00012930414337826196 + }, + { + "days": 0.397222, + "weight": 0.00023493288022247597 + }, + { + "days": 0.398611, + "weight": 0.0002658930272285387 + }, + { + "days": 0.399306, + "weight": 0.00014569480944029518 + }, + { + "days": 0.400694, + "weight": 0.00023857525045848336 + }, + { + "days": 0.404167, + "weight": 0.00014751599455829886 + }, + { + "days": 0.406944, + "weight": 0.0020761510345242064 + }, + { + "days": 0.407639, + "weight": 0.00018211851180036898 + }, + { + "days": 0.409028, + "weight": 0.0001402312540862841 + }, + { + "days": 0.4125, + "weight": 0.0002950319891165977 + }, + { + "days": 0.414583, + "weight": 8.013214519216235e-05 + }, + { + "days": 0.415972, + "weight": 9.834399637219924e-05 + }, + { + "days": 0.418056, + "weight": 0.0003041379147066162 + }, + { + "days": 0.41875, + "weight": 0.00012019821778824352 + }, + { + "days": 0.419444, + "weight": 0.00023857525045848336 + }, + { + "days": 0.420139, + "weight": 0.00034602517242070106 + }, + { + "days": 0.421528, + "weight": 0.0004735081306809593 + }, + { + "days": 0.422222, + "weight": 0.00023675406534047965 + }, + { + "days": 0.422917, + "weight": 9.652281125419556e-05 + }, + { + "days": 0.424306, + "weight": 0.00024403880581249442 + }, + { + "days": 0.425, + "weight": 0.00034420398730269734 + }, + { + "days": 0.426389, + "weight": 0.00013658888385027673 + }, + { + "days": 0.427083, + "weight": 0.00023857525045848336 + }, + { + "days": 0.427778, + "weight": 0.0003041379147066162 + }, + { + "days": 0.429861, + "weight": 0.0017592648239915642 + }, + { + "days": 0.43125, + "weight": 5.4635553540110695e-05 + }, + { + "days": 0.431944, + "weight": 0.00019850917786240217 + }, + { + "days": 0.432639, + "weight": 0.00024221762069449073 + }, + { + "days": 0.434028, + "weight": 0.0002130786588064317 + }, + { + "days": 0.435417, + "weight": 0.00014751599455829886 + }, + { + "days": 0.436806, + "weight": 4.917199818609962e-05 + }, + { + "days": 0.4375, + "weight": 0.0008268180435736752 + }, + { + "days": 0.438194, + "weight": 0.0002458599909304981 + }, + { + "days": 0.438889, + "weight": 0.0003023167295886125 + }, + { + "days": 0.440278, + "weight": 0.00016390666062033208 + }, + { + "days": 0.442361, + "weight": 0.00018758206715438003 + }, + { + "days": 0.443056, + "weight": 0.0001620854755023284 + }, + { + "days": 0.446528, + "weight": 0.00013112532849626567 + }, + { + "days": 0.447222, + "weight": 0.0003733429491907564 + }, + { + "days": 0.447917, + "weight": 0.0002276481397504612 + }, + { + "days": 0.449306, + "weight": 0.00017119140109234684 + }, + { + "days": 0.453472, + "weight": 0.00041523020690484124 + }, + { + "days": 0.457639, + "weight": 0.00040066072596081176 + }, + { + "days": 0.458333, + "weight": 0.00048443524138898147 + }, + { + "days": 0.459028, + "weight": 8.74168856641771e-05 + }, + { + "days": 0.460417, + "weight": 0.00024403880581249442 + }, + { + "days": 0.4625, + "weight": 0.001085426330330199 + }, + { + "days": 0.466667, + "weight": 0.002205455177902468 + }, + { + "days": 0.468056, + "weight": 0.00047715050091696673 + }, + { + "days": 0.470833, + "weight": 5.099318330410331e-05 + }, + { + "days": 0.472222, + "weight": 0.00021489984392443538 + }, + { + "days": 0.472917, + "weight": 0.0001948668076263948 + }, + { + "days": 0.473611, + "weight": 0.0005354284246930847 + }, + { + "days": 0.474306, + "weight": 0.000580958052643177 + }, + { + "days": 0.475, + "weight": 0.000316886210532642 + }, + { + "days": 0.477083, + "weight": 0.0010526449982061327 + }, + { + "days": 0.477778, + "weight": 0.0003533099128927158 + }, + { + "days": 0.479167, + "weight": 0.0005008259074510147 + }, + { + "days": 0.480556, + "weight": 0.0005481767205191106 + }, + { + "days": 0.48125, + "weight": 0.0006028122740592213 + }, + { + "days": 0.483333, + "weight": 0.00047168694556295565 + }, + { + "days": 0.484028, + "weight": 0.001462411649756963 + }, + { + "days": 0.484722, + "weight": 0.00032781332124066416 + }, + { + "days": 0.485417, + "weight": 0.000316886210532642 + }, + { + "days": 0.486806, + "weight": 0.001320359210552675 + }, + { + "days": 0.4875, + "weight": 0.0005937063484692029 + }, + { + "days": 0.488194, + "weight": 0.0003769853194267638 + }, + { + "days": 0.488889, + "weight": 0.0003314556914766715 + }, + { + "days": 0.490278, + "weight": 0.0008668841161697563 + }, + { + "days": 0.490972, + "weight": 0.0002531447314025129 + }, + { + "days": 0.491667, + "weight": 0.0004425479836748966 + }, + { + "days": 0.492361, + "weight": 0.0002822836932905719 + }, + { + "days": 0.493056, + "weight": 0.0008668841161697563 + }, + { + "days": 0.49375, + "weight": 0.0007011562704314205 + }, + { + "days": 0.494444, + "weight": 0.0038499853394598 + }, + { + "days": 0.495833, + "weight": 0.0004243361324948597 + }, + { + "days": 0.498611, + "weight": 0.0008195333031016604 + }, + { + "days": 0.499306, + "weight": 0.00028956843376258664 + }, + { + "days": 0.500694, + "weight": 0.0003223497658866531 + }, + { + "days": 0.502083, + "weight": 0.0003533099128927158 + }, + { + "days": 0.50625, + "weight": 0.0004061242813148228 + }, + { + "days": 0.506944, + "weight": 0.0008322815989276862 + }, + { + "days": 0.509028, + "weight": 0.0004061242813148228 + }, + { + "days": 0.511111, + "weight": 0.0012129092885904574 + }, + { + "days": 0.5125, + "weight": 0.00015297954991230994 + }, + { + "days": 0.517361, + "weight": 0.0004953623520970036 + }, + { + "days": 0.51875, + "weight": 0.00029138961888059035 + }, + { + "days": 0.521528, + "weight": 0.0006009910889412176 + }, + { + "days": 0.522917, + "weight": 0.00029685317423460144 + }, + { + "days": 0.524306, + "weight": 0.00042979968784887077 + }, + { + "days": 0.525, + "weight": 0.00020033036298040588 + }, + { + "days": 0.526389, + "weight": 0.00027864132305456454 + }, + { + "days": 0.527083, + "weight": 0.00020215154809840956 + }, + { + "days": 0.532639, + "weight": 0.00044801153902890767 + }, + { + "days": 0.533333, + "weight": 0.0008049638221576308 + }, + { + "days": 0.535417, + "weight": 0.0004953623520970036 + }, + { + "days": 0.536111, + "weight": 0.00033874043194868626 + }, + { + "days": 0.536806, + "weight": 0.0014988353521170367 + }, + { + "days": 0.538194, + "weight": 9.470162613619187e-05 + }, + { + "days": 0.539583, + "weight": 0.0006301300508292767 + }, + { + "days": 0.540278, + "weight": 0.0006009910889412176 + }, + { + "days": 0.543056, + "weight": 0.0005099318330410331 + }, + { + "days": 0.544444, + "weight": 0.0009433738911259113 + }, + { + "days": 0.545833, + "weight": 0.0004079454664328265 + }, + { + "days": 0.546528, + "weight": 0.0007849307858595903 + }, + { + "days": 0.547222, + "weight": 0.0003551310980107195 + }, + { + "days": 0.548611, + "weight": 0.00022400576951445383 + }, + { + "days": 0.549306, + "weight": 0.0003350980617126789 + }, + { + "days": 0.55, + "weight": 0.0015953581633712322 + }, + { + "days": 0.550694, + "weight": 0.0010599297386781473 + }, + { + "days": 0.551389, + "weight": 0.00045893864973692983 + }, + { + "days": 0.552083, + "weight": 0.00010744992196221769 + }, + { + "days": 0.552778, + "weight": 0.00021672102904243907 + }, + { + "days": 0.553472, + "weight": 0.0005627462014631402 + }, + { + "days": 0.554167, + "weight": 6.738384936613652e-05 + }, + { + "days": 0.554861, + "weight": 0.00028956843376258664 + }, + { + "days": 0.555556, + "weight": 0.00025678710163852027 + }, + { + "days": 0.556944, + "weight": 0.0001930456225083911 + }, + { + "days": 0.557639, + "weight": 0.0003241709510046568 + }, + { + "days": 0.558333, + "weight": 0.0003587734682467269 + }, + { + "days": 0.559722, + "weight": 0.00044072679855689293 + }, + { + "days": 0.560417, + "weight": 0.00011473466243423246 + }, + { + "days": 0.568056, + "weight": 0.00015115836479430626 + }, + { + "days": 0.570139, + "weight": 0.0005864216079971881 + }, + { + "days": 0.570833, + "weight": 9.470162613619187e-05 + }, + { + "days": 0.572917, + "weight": 0.00016390666062033208 + }, + { + "days": 0.575, + "weight": 0.00018393969691837266 + }, + { + "days": 0.58125, + "weight": 0.0002586082867565239 + }, + { + "days": 0.584722, + "weight": 0.0006975139001954132 + }, + { + "days": 0.5875, + "weight": 4.006607259608118e-05 + }, + { + "days": 0.590278, + "weight": 8.013214519216235e-05 + }, + { + "days": 0.591667, + "weight": 0.00040430309619681913 + }, + { + "days": 0.592361, + "weight": 0.0002312905099864686 + }, + { + "days": 0.593056, + "weight": 9.288044101818817e-05 + }, + { + "days": 0.59375, + "weight": 0.00012019821778824352 + }, + { + "days": 0.595833, + "weight": 0.00012930414337826196 + }, + { + "days": 0.597222, + "weight": 0.00027317776770055345 + }, + { + "days": 0.597917, + "weight": 0.0012110881034724536 + }, + { + "days": 0.598611, + "weight": 0.0004279785027308671 + }, + { + "days": 0.599306, + "weight": 9.652281125419556e-05 + }, + { + "days": 0.6, + "weight": 0.0001384100689682804 + }, + { + "days": 0.601389, + "weight": 0.0003023167295886125 + }, + { + "days": 0.602083, + "weight": 6.192029401212545e-05 + }, + { + "days": 0.602778, + "weight": 0.00021854221416044278 + }, + { + "days": 0.603472, + "weight": 0.0002276481397504612 + }, + { + "days": 0.605556, + "weight": 0.00048443524138898147 + }, + { + "days": 0.606944, + "weight": 0.00018211851180036898 + }, + { + "days": 0.607639, + "weight": 0.000158443105266321 + }, + { + "days": 0.609028, + "weight": 0.00016572784573833576 + }, + { + "days": 0.609722, + "weight": 0.00026042947187452763 + }, + { + "days": 0.610417, + "weight": 0.0001384100689682804 + }, + { + "days": 0.611111, + "weight": 0.0018940325227238372 + }, + { + "days": 0.611806, + "weight": 0.0003314556914766715 + }, + { + "days": 0.6125, + "weight": 7.10262196021439e-05 + }, + { + "days": 0.613194, + "weight": 5.4635553540110695e-05 + }, + { + "days": 0.613889, + "weight": 0.00031506502541463834 + }, + { + "days": 0.614583, + "weight": 0.0002859260635265793 + }, + { + "days": 0.615278, + "weight": 8.013214519216235e-05 + }, + { + "days": 0.615972, + "weight": 8.013214519216235e-05 + }, + { + "days": 0.616667, + "weight": 0.0003733429491907564 + }, + { + "days": 0.617361, + "weight": 0.00022946932486846491 + }, + { + "days": 0.61875, + "weight": 0.00019850917786240217 + }, + { + "days": 0.619444, + "weight": 9.105925590018449e-05 + }, + { + "days": 0.620139, + "weight": 0.0003296345063586678 + }, + { + "days": 0.622222, + "weight": 8.559570054617342e-05 + }, + { + "days": 0.623611, + "weight": 0.00010380755172621031 + }, + { + "days": 0.624306, + "weight": 0.00012019821778824352 + }, + { + "days": 0.628472, + "weight": 8.559570054617342e-05 + }, + { + "days": 0.631944, + "weight": 0.00027317776770055345 + }, + { + "days": 0.632639, + "weight": 8.74168856641771e-05 + }, + { + "days": 0.634028, + "weight": 0.0004552962795009224 + }, + { + "days": 0.642361, + "weight": 0.00011473466243423246 + }, + { + "days": 0.644444, + "weight": 0.0005536402758731216 + }, + { + "days": 0.648611, + "weight": 0.00011837703267023983 + }, + { + "days": 0.652778, + "weight": 0.001671847938327387 + }, + { + "days": 0.654167, + "weight": 0.0004680445753269483 + }, + { + "days": 0.654861, + "weight": 0.0001420524392042878 + }, + { + "days": 0.655556, + "weight": 0.00040430309619681913 + }, + { + "days": 0.65625, + "weight": 6.009910889412176e-05 + }, + { + "days": 0.656944, + "weight": 8.195333031016604e-05 + }, + { + "days": 0.657639, + "weight": 5.2814368422107004e-05 + }, + { + "days": 0.658333, + "weight": 0.00021672102904243907 + }, + { + "days": 0.659028, + "weight": 7.466858983815127e-05 + }, + { + "days": 0.660417, + "weight": 0.00024039643557648705 + }, + { + "days": 0.661806, + "weight": 0.0001802973266823653 + }, + { + "days": 0.6625, + "weight": 7.648977495615497e-05 + }, + { + "days": 0.663194, + "weight": 0.00011109229219822507 + }, + { + "days": 0.663889, + "weight": 6.009910889412176e-05 + }, + { + "days": 0.664583, + "weight": 0.0008923807078218079 + }, + { + "days": 0.665278, + "weight": 0.00012930414337826196 + }, + { + "days": 0.665972, + "weight": 0.0001802973266823653 + }, + { + "days": 0.666667, + "weight": 0.00011655584755223614 + }, + { + "days": 0.667361, + "weight": 0.000158443105266321 + }, + { + "days": 0.668056, + "weight": 4.188725771408486e-05 + }, + { + "days": 0.66875, + "weight": 0.0010471814428521216 + }, + { + "days": 0.669444, + "weight": 0.00021672102904243907 + }, + { + "days": 0.670139, + "weight": 0.0016937021597434314 + }, + { + "days": 0.670833, + "weight": 0.000158443105266321 + }, + { + "days": 0.671528, + "weight": 0.0001420524392042878 + }, + { + "days": 0.672917, + "weight": 0.0002476811760485018 + }, + { + "days": 0.673611, + "weight": 0.00021489984392443538 + }, + { + "days": 0.674306, + "weight": 0.00016937021597434313 + }, + { + "days": 0.675, + "weight": 7.831096007415866e-05 + }, + { + "days": 0.675694, + "weight": 0.0002513235462845092 + }, + { + "days": 0.676389, + "weight": 0.0001948668076263948 + }, + { + "days": 0.677083, + "weight": 0.00023311169510447228 + }, + { + "days": 0.678472, + "weight": 0.00010016518149020294 + }, + { + "days": 0.68125, + "weight": 6.009910889412176e-05 + }, + { + "days": 0.681944, + "weight": 0.0002822836932905719 + }, + { + "days": 0.682639, + "weight": 0.00012748295826025828 + }, + { + "days": 0.683333, + "weight": 0.0003915548003707933 + }, + { + "days": 0.684028, + "weight": 0.00045165390926491504 + }, + { + "days": 0.6875, + "weight": 0.00013476769873227304 + }, + { + "days": 0.688889, + "weight": 0.0001802973266823653 + }, + { + "days": 0.690278, + "weight": 5.645673865811438e-05 + }, + { + "days": 0.69375, + "weight": 0.00010380755172621031 + }, + { + "days": 0.695833, + "weight": 6.92050344841402e-05 + }, + { + "days": 0.698611, + "weight": 0.0009051290036478338 + }, + { + "days": 0.70625, + "weight": 3.278133212406642e-05 + }, + { + "days": 0.707639, + "weight": 0.00011473466243423246 + }, + { + "days": 0.713889, + "weight": 0.00043708442832088556 + }, + { + "days": 0.716667, + "weight": 0.0001620854755023284 + }, + { + "days": 0.71875, + "weight": 0.0008468510798717157 + }, + { + "days": 0.719444, + "weight": 0.0007794672305055792 + }, + { + "days": 0.720139, + "weight": 0.001537080239595114 + }, + { + "days": 0.720833, + "weight": 0.0003915548003707933 + }, + { + "days": 0.721528, + "weight": 0.00020579391833441693 + }, + { + "days": 0.722222, + "weight": 9.834399637219924e-05 + }, + { + "days": 0.723611, + "weight": 0.00021489984392443538 + }, + { + "days": 0.725, + "weight": 9.105925590018449e-05 + }, + { + "days": 0.725694, + "weight": 0.00012201940290624721 + }, + { + "days": 0.726389, + "weight": 0.0003788065045447675 + }, + { + "days": 0.727083, + "weight": 0.00011473466243423246 + }, + { + "days": 0.727778, + "weight": 0.0001566219201483173 + }, + { + "days": 0.728472, + "weight": 0.00024039643557648705 + }, + { + "days": 0.729167, + "weight": 0.00031142265517863097 + }, + { + "days": 0.729861, + "weight": 0.00011291347731622876 + }, + { + "days": 0.730556, + "weight": 0.0013749947640927858 + }, + { + "days": 0.73125, + "weight": 0.00010016518149020294 + }, + { + "days": 0.731944, + "weight": 7.648977495615497e-05 + }, + { + "days": 0.732639, + "weight": 0.0001402312540862841 + }, + { + "days": 0.733333, + "weight": 8.195333031016604e-05 + }, + { + "days": 0.734028, + "weight": 0.00026225065699253135 + }, + { + "days": 0.734722, + "weight": 9.288044101818817e-05 + }, + { + "days": 0.735417, + "weight": 0.00035695228312872316 + }, + { + "days": 0.736806, + "weight": 0.00010562873684421401 + }, + { + "days": 0.7375, + "weight": 0.00015297954991230994 + }, + { + "days": 0.738194, + "weight": 0.0007157257513754501 + }, + { + "days": 0.738889, + "weight": 0.0002768201379365608 + }, + { + "days": 0.740278, + "weight": 0.00014751599455829886 + }, + { + "days": 0.740972, + "weight": 6.374147913012914e-05 + }, + { + "days": 0.741667, + "weight": 0.00011291347731622876 + }, + { + "days": 0.743056, + "weight": 4.370844283208855e-05 + }, + { + "days": 0.744444, + "weight": 0.00010744992196221769 + }, + { + "days": 0.746528, + "weight": 0.00010380755172621031 + }, + { + "days": 0.747222, + "weight": 0.0005627462014631402 + }, + { + "days": 0.748611, + "weight": 0.00013658888385027673 + }, + { + "days": 0.749306, + "weight": 0.00015115836479430626 + }, + { + "days": 0.75, + "weight": 6.374147913012914e-05 + }, + { + "days": 0.750694, + "weight": 0.00012201940290624721 + }, + { + "days": 0.7625, + "weight": 7.284740472014759e-05 + }, + { + "days": 0.776389, + "weight": 0.00023857525045848336 + }, + { + "days": 0.777083, + "weight": 6.556266424813284e-05 + }, + { + "days": 0.777778, + "weight": 6.009910889412176e-05 + }, + { + "days": 0.778472, + "weight": 0.0005299648693390737 + }, + { + "days": 0.779167, + "weight": 0.00020215154809840956 + }, + { + "days": 0.779861, + "weight": 0.0025223413884351105 + }, + { + "days": 0.780556, + "weight": 0.00046986576044495194 + }, + { + "days": 0.78125, + "weight": 0.00011473466243423246 + }, + { + "days": 0.781944, + "weight": 0.00010198636660820662 + }, + { + "days": 0.782639, + "weight": 0.0003478463575387047 + }, + { + "days": 0.783333, + "weight": 9.834399637219924e-05 + }, + { + "days": 0.784028, + "weight": 0.00021489984392443538 + }, + { + "days": 0.785417, + "weight": 0.0004735081306809593 + }, + { + "days": 0.786806, + "weight": 0.000316886210532642 + }, + { + "days": 0.7875, + "weight": 0.002899326707861874 + }, + { + "days": 0.788194, + "weight": 0.0006173817550032509 + }, + { + "days": 0.788889, + "weight": 4.7350813068095936e-05 + }, + { + "days": 0.789583, + "weight": 0.0008177121179836567 + }, + { + "days": 0.790972, + "weight": 7.648977495615497e-05 + }, + { + "days": 0.791667, + "weight": 0.00036423702360073796 + }, + { + "days": 0.792361, + "weight": 0.0003205285807686494 + }, + { + "days": 0.793056, + "weight": 0.00010198636660820662 + }, + { + "days": 0.79375, + "weight": 0.0001420524392042878 + }, + { + "days": 0.795833, + "weight": 0.0004571174646189261 + }, + { + "days": 0.796528, + "weight": 0.0001620854755023284 + }, + { + "days": 0.797917, + "weight": 0.00019122443739038743 + }, + { + "days": 0.798611, + "weight": 0.0003951971706068007 + }, + { + "days": 0.799306, + "weight": 0.00010927110708022139 + }, + { + "days": 0.8, + "weight": 0.00020397273321641325 + }, + { + "days": 0.800694, + "weight": 6.92050344841402e-05 + }, + { + "days": 0.801389, + "weight": 0.00041158783666883387 + }, + { + "days": 0.802778, + "weight": 9.652281125419556e-05 + }, + { + "days": 0.804167, + "weight": 0.00012201940290624721 + }, + { + "days": 0.804861, + "weight": 0.0003733429491907564 + }, + { + "days": 0.805556, + "weight": 9.105925590018449e-05 + }, + { + "days": 0.80625, + "weight": 6.556266424813284e-05 + }, + { + "days": 0.808333, + "weight": 0.00010198636660820662 + }, + { + "days": 0.809028, + "weight": 0.00010380755172621031 + }, + { + "days": 0.810417, + "weight": 0.0003951971706068007 + }, + { + "days": 0.811111, + "weight": 0.0003023167295886125 + }, + { + "days": 0.827083, + "weight": 0.00011109229219822507 + }, + { + "days": 0.832639, + "weight": 0.00018940325227238374 + }, + { + "days": 0.836806, + "weight": 8.377451542816972e-05 + }, + { + "days": 0.838194, + "weight": 0.0003788065045447675 + }, + { + "days": 0.838889, + "weight": 0.0009761552232499777 + }, + { + "days": 0.839583, + "weight": 0.0005317860544570774 + }, + { + "days": 0.840278, + "weight": 0.00010198636660820662 + }, + { + "days": 0.840972, + "weight": 5.827792377611807e-05 + }, + { + "days": 0.841667, + "weight": 0.00024950236116650547 + }, + { + "days": 0.842361, + "weight": 0.00011655584755223614 + }, + { + "days": 0.84375, + "weight": 0.00037516413430876007 + }, + { + "days": 0.844444, + "weight": 0.00016572784573833576 + }, + { + "days": 0.845833, + "weight": 0.000287747248644583 + }, + { + "days": 0.846528, + "weight": 0.0007812884156235829 + }, + { + "days": 0.847222, + "weight": 0.0010380755172621032 + }, + { + "days": 0.849306, + "weight": 0.0031196901071403205 + }, + { + "days": 0.85, + "weight": 0.00019122443739038743 + }, + { + "days": 0.850694, + "weight": 0.0005408919800470958 + }, + { + "days": 0.851389, + "weight": 0.0002531447314025129 + }, + { + "days": 0.852778, + "weight": 8.377451542816972e-05 + }, + { + "days": 0.854167, + "weight": 9.652281125419556e-05 + }, + { + "days": 0.854861, + "weight": 0.0006082758294132323 + }, + { + "days": 0.855556, + "weight": 0.000264071842110535 + }, + { + "days": 0.856944, + "weight": 0.0003205285807686494 + }, + { + "days": 0.857639, + "weight": 0.00012019821778824352 + }, + { + "days": 0.858333, + "weight": 0.00010927110708022139 + }, + { + "days": 0.860417, + "weight": 0.00026225065699253135 + }, + { + "days": 0.861111, + "weight": 0.0005190377586310516 + }, + { + "days": 0.861806, + "weight": 0.00015480073503031363 + }, + { + "days": 0.863889, + "weight": 0.00042979968784887077 + }, + { + "days": 0.864583, + "weight": 0.00020943628857042433 + }, + { + "days": 0.865278, + "weight": 0.00033874043194868626 + }, + { + "days": 0.865972, + "weight": 0.00019668799274439848 + }, + { + "days": 0.866667, + "weight": 0.0001420524392042878 + }, + { + "days": 0.868056, + "weight": 0.00018393969691837266 + }, + { + "days": 0.86875, + "weight": 0.0003369192468306826 + }, + { + "days": 0.870139, + "weight": 0.0003023167295886125 + }, + { + "days": 0.870833, + "weight": 0.00027135658258254974 + }, + { + "days": 0.872222, + "weight": 0.00026042947187452763 + }, + { + "days": 0.882639, + "weight": 0.00012201940290624721 + }, + { + "days": 0.890278, + "weight": 6.374147913012914e-05 + }, + { + "days": 0.891667, + "weight": 0.00036241583848273425 + }, + { + "days": 0.896528, + "weight": 0.00052814368422107 + }, + { + "days": 0.897917, + "weight": 0.00012019821778824352 + }, + { + "days": 0.898611, + "weight": 0.00047715050091696673 + }, + { + "days": 0.901389, + "weight": 0.0002768201379365608 + }, + { + "days": 0.902083, + "weight": 0.0001238405880242509 + }, + { + "days": 0.902778, + "weight": 0.00025496591652051655 + }, + { + "days": 0.903472, + "weight": 0.00024221762069449073 + }, + { + "days": 0.904861, + "weight": 0.00037516413430876007 + }, + { + "days": 0.905556, + "weight": 0.004046673332204199 + }, + { + "days": 0.90625, + "weight": 0.0006374147913012914 + }, + { + "days": 0.906944, + "weight": 0.000657447827599332 + }, + { + "days": 0.907639, + "weight": 0.0004571174646189261 + }, + { + "days": 0.908333, + "weight": 0.0005153953883950442 + }, + { + "days": 0.909722, + "weight": 8.377451542816972e-05 + }, + { + "days": 0.913194, + "weight": 0.00020761510345242062 + }, + { + "days": 0.913889, + "weight": 0.00024950236116650547 + }, + { + "days": 0.914583, + "weight": 0.0004443691687929003 + }, + { + "days": 0.915278, + "weight": 0.0001256617731422546 + }, + { + "days": 0.915972, + "weight": 0.000921519669709867 + }, + { + "days": 0.916667, + "weight": 0.00044619035391090396 + }, + { + "days": 0.917361, + "weight": 3.8244887478077486e-05 + }, + { + "days": 0.918056, + "weight": 0.0002276481397504612 + }, + { + "days": 0.921528, + "weight": 0.011948795559222208 + }, + { + "days": 0.922222, + "weight": 0.00023857525045848336 + }, + { + "days": 0.922917, + "weight": 0.0009634069274239519 + }, + { + "days": 0.923611, + "weight": 0.00033327687659467524 + }, + { + "days": 0.924306, + "weight": 0.0027791284900736306 + }, + { + "days": 0.925, + "weight": 0.00021854221416044278 + }, + { + "days": 0.925694, + "weight": 0.0005153953883950442 + }, + { + "days": 0.926389, + "weight": 0.00011109229219822507 + }, + { + "days": 0.927083, + "weight": 0.0002130786588064317 + }, + { + "days": 0.930556, + "weight": 0.0006009910889412176 + }, + { + "days": 0.93125, + "weight": 0.0003551310980107195 + }, + { + "days": 0.931944, + "weight": 0.00019668799274439848 + }, + { + "days": 0.932639, + "weight": 0.00030960147006062726 + }, + { + "days": 0.933333, + "weight": 0.0003551310980107195 + }, + { + "days": 0.94375, + "weight": 0.0004735081306809593 + }, + { + "days": 0.948611, + "weight": 0.00024221762069449073 + }, + { + "days": 0.950694, + "weight": 0.00032781332124066416 + }, + { + "days": 0.951389, + "weight": 0.0013003261742546345 + }, + { + "days": 0.955556, + "weight": 0.0008286392286916788 + }, + { + "days": 0.95625, + "weight": 0.0029539622614019848 + }, + { + "days": 0.956944, + "weight": 0.001016221295846059 + }, + { + "days": 0.957639, + "weight": 0.0003478463575387047 + }, + { + "days": 0.958333, + "weight": 0.00023675406534047965 + }, + { + "days": 0.959028, + "weight": 0.0006009910889412176 + }, + { + "days": 0.959722, + "weight": 6.92050344841402e-05 + }, + { + "days": 0.960417, + "weight": 0.0036369066806533684 + }, + { + "days": 0.961111, + "weight": 0.001819363932885686 + }, + { + "days": 0.961806, + "weight": 0.00023857525045848336 + }, + { + "days": 0.9625, + "weight": 0.00139684898550883 + }, + { + "days": 0.964583, + "weight": 0.0011345983285162987 + }, + { + "days": 0.965278, + "weight": 0.0005245013139850627 + }, + { + "days": 0.965972, + "weight": 0.0015862522377812138 + }, + { + "days": 0.968056, + "weight": 0.0009124137441198486 + }, + { + "days": 0.96875, + "weight": 0.007162721069108512 + }, + { + "days": 0.970139, + "weight": 0.0004971835372150073 + }, + { + "days": 0.972222, + "weight": 0.0018047944519416566 + }, + { + "days": 0.972917, + "weight": 0.008789039379485806 + }, + { + "days": 0.973611, + "weight": 0.0015589344610111584 + }, + { + "days": 0.974306, + "weight": 0.00161903356990528 + }, + { + "days": 0.975, + "weight": 0.0007776460453875755 + }, + { + "days": 0.975694, + "weight": 0.00025496591652051655 + }, + { + "days": 0.976389, + "weight": 0.0009579433720699408 + }, + { + "days": 0.977083, + "weight": 0.004013892000080132 + }, + { + "days": 0.977778, + "weight": 4.7350813068095936e-05 + }, + { + "days": 0.979167, + "weight": 0.001719198751395483 + }, + { + "days": 0.979861, + "weight": 0.0008778112268777784 + }, + { + "days": 0.980556, + "weight": 0.0013076109147266493 + }, + { + "days": 0.98125, + "weight": 0.0017501588984015458 + }, + { + "days": 0.981944, + "weight": 0.003964720001894032 + }, + { + "days": 0.982639, + "weight": 0.005221337733316579 + }, + { + "days": 0.983333, + "weight": 0.0012165516588264647 + }, + { + "days": 0.984722, + "weight": 0.00041705139202284495 + }, + { + "days": 0.985417, + "weight": 0.0009561221869519371 + }, + { + "days": 0.986111, + "weight": 0.008585066646269393 + }, + { + "days": 0.986806, + "weight": 0.0003296345063586678 + }, + { + "days": 0.988194, + "weight": 0.010790521824171862 + }, + { + "days": 0.988889, + "weight": 0.001132777143398295 + }, + { + "days": 0.989583, + "weight": 0.0004279785027308671 + }, + { + "days": 0.990278, + "weight": 0.0018776418566618042 + }, + { + "days": 0.990972, + "weight": 0.003469357649797029 + }, + { + "days": 0.991667, + "weight": 4.188725771408486e-05 + }, + { + "days": 0.995139, + "weight": 0.001150988994578332 + }, + { + "days": 0.995833, + "weight": 0.00014751599455829886 + }, + { + "days": 0.998611, + "weight": 0.0005682097568171512 + }, + { + "days": 1.002778, + "weight": 0.0004279785027308671 + }, + { + "days": 1.007639, + "weight": 0.001544364980067129 + }, + { + "days": 1.008333, + "weight": 4.7350813068095936e-05 + }, + { + "days": 1.009722, + "weight": 0.003493033056331077 + }, + { + "days": 1.011111, + "weight": 0.0004261573176128634 + }, + { + "days": 1.014583, + "weight": 9.105925590018449e-05 + }, + { + "days": 1.015278, + "weight": 0.002493202426547051 + }, + { + "days": 1.016667, + "weight": 0.002449493983714963 + }, + { + "days": 1.017361, + "weight": 0.0018794630417798077 + }, + { + "days": 1.018056, + "weight": 0.022730211457804052 + }, + { + "days": 1.01875, + "weight": 0.0021034688112942616 + }, + { + "days": 1.020139, + "weight": 0.0006283088657112729 + }, + { + "days": 1.021528, + "weight": 0.00019122443739038743 + }, + { + "days": 1.022917, + "weight": 0.0015807886824272027 + }, + { + "days": 1.023611, + "weight": 0.00022036339927844646 + }, + { + "days": 1.024306, + "weight": 0.0004243361324948597 + }, + { + "days": 1.025, + "weight": 0.0002804625081725682 + }, + { + "days": 1.027083, + "weight": 0.0008304604138096825 + }, + { + "days": 1.027778, + "weight": 0.005159417439304453 + }, + { + "days": 1.028472, + "weight": 0.00010927110708022139 + }, + { + "days": 1.029861, + "weight": 0.0021617467350703795 + }, + { + "days": 1.03125, + "weight": 0.00024039643557648705 + }, + { + "days": 1.031944, + "weight": 0.0024458516134789553 + }, + { + "days": 1.032639, + "weight": 0.002223667029082505 + }, + { + "days": 1.033333, + "weight": 0.0002677142123465424 + }, + { + "days": 1.034028, + "weight": 0.001338571061732712 + }, + { + "days": 1.034722, + "weight": 0.005957096520990069 + }, + { + "days": 1.035417, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.036111, + "weight": 0.001415060836688867 + }, + { + "days": 1.036806, + "weight": 0.00028956843376258664 + }, + { + "days": 1.038889, + "weight": 0.000921519669709867 + }, + { + "days": 1.039583, + "weight": 0.0011764855862303836 + }, + { + "days": 1.040972, + "weight": 0.000604633459177225 + }, + { + "days": 1.041667, + "weight": 0.0006501630871273172 + }, + { + "days": 1.042361, + "weight": 0.0014696963902289775 + }, + { + "days": 1.043056, + "weight": 0.002342044061752745 + }, + { + "days": 1.04375, + "weight": 0.0013768159492107894 + }, + { + "days": 1.044444, + "weight": 0.0005208589437490553 + }, + { + "days": 1.045139, + "weight": 0.0016955233448614352 + }, + { + "days": 1.045833, + "weight": 0.000686586789487391 + }, + { + "days": 1.046528, + "weight": 0.0008450298947537121 + }, + { + "days": 1.048611, + "weight": 0.000662911382953343 + }, + { + "days": 1.05, + "weight": 0.0007175469364934538 + }, + { + "days": 1.050694, + "weight": 0.0005299648693390737 + }, + { + "days": 1.051389, + "weight": 0.025820762603056314 + }, + { + "days": 1.052083, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.052778, + "weight": 5.645673865811438e-05 + }, + { + "days": 1.054861, + "weight": 0.003372834838542833 + }, + { + "days": 1.056944, + "weight": 0.0003842700598987785 + }, + { + "days": 1.058333, + "weight": 0.0005827792377611807 + }, + { + "days": 1.064583, + "weight": 0.00028956843376258664 + }, + { + "days": 1.065972, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.069444, + "weight": 0.0005172165735130478 + }, + { + "days": 1.073611, + "weight": 0.00043708442832088556 + }, + { + "days": 1.074306, + "weight": 0.0001438736243222915 + }, + { + "days": 1.076389, + "weight": 0.0003897336152527896 + }, + { + "days": 1.077083, + "weight": 0.00034420398730269734 + }, + { + "days": 1.077778, + "weight": 0.00021854221416044278 + }, + { + "days": 1.079167, + "weight": 0.00010016518149020294 + }, + { + "days": 1.079861, + "weight": 0.00018576088203637635 + }, + { + "days": 1.08125, + "weight": 0.00012201940290624721 + }, + { + "days": 1.082639, + "weight": 0.000686586789487391 + }, + { + "days": 1.084028, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.084722, + "weight": 0.0017465165281655385 + }, + { + "days": 1.086111, + "weight": 0.0002531447314025129 + }, + { + "days": 1.0875, + "weight": 8.559570054617342e-05 + }, + { + "days": 1.089583, + "weight": 0.0004024819110788154 + }, + { + "days": 1.090972, + "weight": 0.00023311169510447228 + }, + { + "days": 1.091667, + "weight": 0.0009670492976599593 + }, + { + "days": 1.092361, + "weight": 0.0014642328348749665 + }, + { + "days": 1.09375, + "weight": 0.0006119181996492398 + }, + { + "days": 1.094444, + "weight": 0.0002130786588064317 + }, + { + "days": 1.095139, + "weight": 0.00030960147006062726 + }, + { + "days": 1.097917, + "weight": 0.0017119140109234684 + }, + { + "days": 1.098611, + "weight": 0.00011655584755223614 + }, + { + "days": 1.1, + "weight": 0.00019122443739038743 + }, + { + "days": 1.100694, + "weight": 0.0005463555354011069 + }, + { + "days": 1.101389, + "weight": 0.0005226801288670589 + }, + { + "days": 1.102083, + "weight": 0.00044619035391090396 + }, + { + "days": 1.104167, + "weight": 0.00026225065699253135 + }, + { + "days": 1.104861, + "weight": 0.00018758206715438003 + }, + { + "days": 1.105556, + "weight": 9.288044101818817e-05 + }, + { + "days": 1.10625, + "weight": 0.0002658930272285387 + }, + { + "days": 1.108333, + "weight": 0.0002695353974645461 + }, + { + "days": 1.109028, + "weight": 8.74168856641771e-05 + }, + { + "days": 1.109722, + "weight": 9.834399637219924e-05 + }, + { + "days": 1.110417, + "weight": 0.00028956843376258664 + }, + { + "days": 1.111806, + "weight": 0.00023675406534047965 + }, + { + "days": 1.1125, + "weight": 0.0002859260635265793 + }, + { + "days": 1.113889, + "weight": 0.00017301258621035053 + }, + { + "days": 1.115972, + "weight": 9.288044101818817e-05 + }, + { + "days": 1.116667, + "weight": 0.00016572784573833576 + }, + { + "days": 1.118056, + "weight": 0.000575494497289166 + }, + { + "days": 1.120139, + "weight": 0.0005336072395750811 + }, + { + "days": 1.120833, + "weight": 0.011540850092789382 + }, + { + "days": 1.131944, + "weight": 8.377451542816972e-05 + }, + { + "days": 1.136111, + "weight": 0.00020761510345242062 + }, + { + "days": 1.136806, + "weight": 0.00025496591652051655 + }, + { + "days": 1.1375, + "weight": 0.00020215154809840956 + }, + { + "days": 1.138889, + "weight": 0.0004990047223330109 + }, + { + "days": 1.140278, + "weight": 5.645673865811438e-05 + }, + { + "days": 1.141667, + "weight": 0.00010927110708022139 + }, + { + "days": 1.145833, + "weight": 0.00014933717967630255 + }, + { + "days": 1.146528, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.147222, + "weight": 0.00012019821778824352 + }, + { + "days": 1.147917, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.148611, + "weight": 9.105925590018449e-05 + }, + { + "days": 1.149306, + "weight": 0.0001238405880242509 + }, + { + "days": 1.15, + "weight": 0.0001438736243222915 + }, + { + "days": 1.150694, + "weight": 0.0003660582087187416 + }, + { + "days": 1.151389, + "weight": 0.0006392359764192951 + }, + { + "days": 1.152083, + "weight": 0.00013112532849626567 + }, + { + "days": 1.153472, + "weight": 0.0001602642903843247 + }, + { + "days": 1.154861, + "weight": 0.0001420524392042878 + }, + { + "days": 1.155556, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.156944, + "weight": 5.2814368422107004e-05 + }, + { + "days": 1.157639, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.159028, + "weight": 0.0004862564265069852 + }, + { + "days": 1.159722, + "weight": 0.00048807761162498884 + }, + { + "days": 1.160417, + "weight": 0.0015243319437690884 + }, + { + "days": 1.161111, + "weight": 0.00029138961888059035 + }, + { + "days": 1.161806, + "weight": 7.466858983815127e-05 + }, + { + "days": 1.163194, + "weight": 0.0001566219201483173 + }, + { + "days": 1.163889, + "weight": 0.00036970057895474904 + }, + { + "days": 1.165278, + "weight": 0.00033327687659467524 + }, + { + "days": 1.165972, + "weight": 5.4635553540110695e-05 + }, + { + "days": 1.166667, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.167361, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.168056, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.170139, + "weight": 8.559570054617342e-05 + }, + { + "days": 1.170833, + "weight": 0.00013112532849626567 + }, + { + "days": 1.171528, + "weight": 0.00016572784573833576 + }, + { + "days": 1.172917, + "weight": 6.738384936613652e-05 + }, + { + "days": 1.174306, + "weight": 0.00029321080399859407 + }, + { + "days": 1.176389, + "weight": 0.00023675406534047965 + }, + { + "days": 1.177083, + "weight": 0.00020215154809840956 + }, + { + "days": 1.178472, + "weight": 0.00011473466243423246 + }, + { + "days": 1.181944, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.184722, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.1875, + "weight": 0.0001948668076263948 + }, + { + "days": 1.189583, + "weight": 3.096014700606273e-05 + }, + { + "days": 1.191667, + "weight": 0.00010016518149020294 + }, + { + "days": 1.193056, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.19375, + "weight": 0.0001802973266823653 + }, + { + "days": 1.194444, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.198611, + "weight": 0.00010016518149020294 + }, + { + "days": 1.199306, + "weight": 0.0019869129637420254 + }, + { + "days": 1.200694, + "weight": 4.5529627950092245e-05 + }, + { + "days": 1.202778, + "weight": 0.00011109229219822507 + }, + { + "days": 1.203472, + "weight": 6.738384936613652e-05 + }, + { + "days": 1.204861, + "weight": 0.00010380755172621031 + }, + { + "days": 1.205556, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.206944, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.209028, + "weight": 0.0001948668076263948 + }, + { + "days": 1.209722, + "weight": 9.834399637219924e-05 + }, + { + "days": 1.210417, + "weight": 0.0005172165735130478 + }, + { + "days": 1.211111, + "weight": 5.4635553540110695e-05 + }, + { + "days": 1.2125, + "weight": 0.0002822836932905719 + }, + { + "days": 1.213194, + "weight": 0.00011655584755223614 + }, + { + "days": 1.213889, + "weight": 0.0002130786588064317 + }, + { + "days": 1.214583, + "weight": 0.00015115836479430626 + }, + { + "days": 1.215278, + "weight": 0.00013476769873227304 + }, + { + "days": 1.215972, + "weight": 5.2814368422107004e-05 + }, + { + "days": 1.218056, + "weight": 0.00015480073503031363 + }, + { + "days": 1.219444, + "weight": 0.0001438736243222915 + }, + { + "days": 1.220139, + "weight": 4.7350813068095936e-05 + }, + { + "days": 1.220833, + "weight": 0.0008814535971137859 + }, + { + "days": 1.222222, + "weight": 0.0003769853194267638 + }, + { + "days": 1.222917, + "weight": 7.466858983815127e-05 + }, + { + "days": 1.224306, + "weight": 0.0001438736243222915 + }, + { + "days": 1.225694, + "weight": 0.0010836051452121954 + }, + { + "days": 1.226389, + "weight": 0.0001238405880242509 + }, + { + "days": 1.228472, + "weight": 0.00015115836479430626 + }, + { + "days": 1.229167, + "weight": 5.645673865811438e-05 + }, + { + "days": 1.229861, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.231944, + "weight": 0.00016937021597434313 + }, + { + "days": 1.232639, + "weight": 0.00010562873684421401 + }, + { + "days": 1.233333, + "weight": 0.00013658888385027673 + }, + { + "days": 1.234028, + "weight": 0.0001748337713283542 + }, + { + "days": 1.234722, + "weight": 8.923807078218079e-05 + }, + { + "days": 1.2375, + "weight": 0.0004735081306809593 + }, + { + "days": 1.238194, + "weight": 9.288044101818817e-05 + }, + { + "days": 1.238889, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.240278, + "weight": 0.00018393969691837266 + }, + { + "days": 1.246528, + "weight": 0.0019504892613819516 + }, + { + "days": 1.251389, + "weight": 7.831096007415866e-05 + }, + { + "days": 1.254167, + "weight": 0.0001930456225083911 + }, + { + "days": 1.25625, + "weight": 0.00012748295826025828 + }, + { + "days": 1.258333, + "weight": 0.00013294651361426936 + }, + { + "days": 1.261806, + "weight": 0.0002859260635265793 + }, + { + "days": 1.263889, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.264583, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.265972, + "weight": 0.00012201940290624721 + }, + { + "days": 1.267361, + "weight": 0.00027135658258254974 + }, + { + "days": 1.268056, + "weight": 0.00021489984392443538 + }, + { + "days": 1.26875, + "weight": 0.0001402312540862841 + }, + { + "days": 1.270139, + "weight": 0.00010016518149020294 + }, + { + "days": 1.272222, + "weight": 0.00011473466243423246 + }, + { + "days": 1.273611, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.274306, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.275, + "weight": 0.00015480073503031363 + }, + { + "days": 1.276389, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.277083, + "weight": 0.00017119140109234684 + }, + { + "days": 1.278472, + "weight": 0.00010562873684421401 + }, + { + "days": 1.279167, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.279861, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.281944, + "weight": 4.5529627950092245e-05 + }, + { + "days": 1.282639, + "weight": 0.00035148872777471214 + }, + { + "days": 1.283333, + "weight": 0.00012201940290624721 + }, + { + "days": 1.284028, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.284722, + "weight": 0.0004243361324948597 + }, + { + "days": 1.285417, + "weight": 0.000158443105266321 + }, + { + "days": 1.286111, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.288194, + "weight": 0.0003660582087187416 + }, + { + "days": 1.290278, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.29375, + "weight": 0.00015297954991230994 + }, + { + "days": 1.294444, + "weight": 0.00013112532849626567 + }, + { + "days": 1.295139, + "weight": 4.7350813068095936e-05 + }, + { + "days": 1.295833, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.296528, + "weight": 0.00018940325227238374 + }, + { + "days": 1.297917, + "weight": 0.00013112532849626567 + }, + { + "days": 1.298611, + "weight": 0.0005882427931151918 + }, + { + "days": 1.299306, + "weight": 0.00016937021597434313 + }, + { + "days": 1.303472, + "weight": 0.0002841048784085756 + }, + { + "days": 1.306944, + "weight": 0.00011837703267023983 + }, + { + "days": 1.313194, + "weight": 0.00010744992196221769 + }, + { + "days": 1.315972, + "weight": 7.466858983815127e-05 + }, + { + "days": 1.316667, + "weight": 5.645673865811438e-05 + }, + { + "days": 1.317361, + "weight": 5.2814368422107004e-05 + }, + { + "days": 1.322917, + "weight": 0.00010744992196221769 + }, + { + "days": 1.325, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.325694, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.326389, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.327083, + "weight": 0.00010380755172621031 + }, + { + "days": 1.327778, + "weight": 3.46025172420701e-05 + }, + { + "days": 1.328472, + "weight": 0.00018393969691837266 + }, + { + "days": 1.329167, + "weight": 5.645673865811438e-05 + }, + { + "days": 1.329861, + "weight": 0.00010380755172621031 + }, + { + "days": 1.330556, + "weight": 0.00011291347731622876 + }, + { + "days": 1.33125, + "weight": 0.0007266528620834721 + }, + { + "days": 1.332639, + "weight": 0.0001238405880242509 + }, + { + "days": 1.333333, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.334722, + "weight": 0.00013658888385027673 + }, + { + "days": 1.336806, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.338194, + "weight": 0.0001238405880242509 + }, + { + "days": 1.338889, + "weight": 0.00010016518149020294 + }, + { + "days": 1.339583, + "weight": 9.470162613619187e-05 + }, + { + "days": 1.340278, + "weight": 5.645673865811438e-05 + }, + { + "days": 1.340972, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.342361, + "weight": 0.00040430309619681913 + }, + { + "days": 1.343056, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.344444, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.345139, + "weight": 0.00013112532849626567 + }, + { + "days": 1.346528, + "weight": 0.00021854221416044278 + }, + { + "days": 1.348611, + "weight": 0.00023857525045848336 + }, + { + "days": 1.35, + "weight": 0.00020943628857042433 + }, + { + "days": 1.352083, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.352778, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.353472, + "weight": 0.00010744992196221769 + }, + { + "days": 1.354167, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.354861, + "weight": 6.92050344841402e-05 + }, + { + "days": 1.355556, + "weight": 0.00011109229219822507 + }, + { + "days": 1.357639, + "weight": 0.00016572784573833576 + }, + { + "days": 1.360417, + "weight": 0.000580958052643177 + }, + { + "days": 1.361111, + "weight": 0.00011837703267023983 + }, + { + "days": 1.363194, + "weight": 0.0001256617731422546 + }, + { + "days": 1.372222, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.372917, + "weight": 0.00012930414337826196 + }, + { + "days": 1.377083, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.379861, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.382639, + "weight": 0.00010380755172621031 + }, + { + "days": 1.385417, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.386111, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.386806, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.388194, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.388889, + "weight": 4.7350813068095936e-05 + }, + { + "days": 1.389583, + "weight": 9.288044101818817e-05 + }, + { + "days": 1.390972, + "weight": 7.466858983815127e-05 + }, + { + "days": 1.391667, + "weight": 0.00011473466243423246 + }, + { + "days": 1.392361, + "weight": 0.0001402312540862841 + }, + { + "days": 1.393056, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.395139, + "weight": 4.5529627950092245e-05 + }, + { + "days": 1.396528, + "weight": 3.46025172420701e-05 + }, + { + "days": 1.397222, + "weight": 0.0003205285807686494 + }, + { + "days": 1.397917, + "weight": 5.4635553540110695e-05 + }, + { + "days": 1.398611, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.399306, + "weight": 0.0004607598348549335 + }, + { + "days": 1.4, + "weight": 8.923807078218079e-05 + }, + { + "days": 1.402083, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.402778, + "weight": 3.096014700606273e-05 + }, + { + "days": 1.403472, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.404167, + "weight": 0.0003369192468306826 + }, + { + "days": 1.405556, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.407639, + "weight": 0.00012201940290624721 + }, + { + "days": 1.409028, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.409722, + "weight": 3.46025172420701e-05 + }, + { + "days": 1.410417, + "weight": 5.4635553540110695e-05 + }, + { + "days": 1.4125, + "weight": 0.00016572784573833576 + }, + { + "days": 1.413194, + "weight": 0.0001256617731422546 + }, + { + "days": 1.414583, + "weight": 0.0002476811760485018 + }, + { + "days": 1.415972, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.418056, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.41875, + "weight": 6.009910889412176e-05 + }, + { + "days": 1.419444, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.420139, + "weight": 0.00038244887478077486 + }, + { + "days": 1.426389, + "weight": 0.00012019821778824352 + }, + { + "days": 1.430556, + "weight": 9.834399637219924e-05 + }, + { + "days": 1.435417, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.4375, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.440278, + "weight": 4.7350813068095936e-05 + }, + { + "days": 1.441667, + "weight": 4.5529627950092245e-05 + }, + { + "days": 1.445833, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.447222, + "weight": 4.7350813068095936e-05 + }, + { + "days": 1.448611, + "weight": 0.000316886210532642 + }, + { + "days": 1.449306, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.45, + "weight": 5.645673865811438e-05 + }, + { + "days": 1.451389, + "weight": 0.000158443105266321 + }, + { + "days": 1.453472, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.454167, + "weight": 0.00022036339927844646 + }, + { + "days": 1.45625, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.456944, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.458333, + "weight": 0.00014569480944029518 + }, + { + "days": 1.460417, + "weight": 0.00032781332124066416 + }, + { + "days": 1.461111, + "weight": 0.00029321080399859407 + }, + { + "days": 1.463889, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.464583, + "weight": 8.74168856641771e-05 + }, + { + "days": 1.465972, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.468056, + "weight": 6.92050344841402e-05 + }, + { + "days": 1.469444, + "weight": 6.92050344841402e-05 + }, + { + "days": 1.470139, + "weight": 0.00023675406534047965 + }, + { + "days": 1.471528, + "weight": 0.00015115836479430626 + }, + { + "days": 1.472222, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.472917, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.474306, + "weight": 6.009910889412176e-05 + }, + { + "days": 1.475694, + "weight": 8.377451542816972e-05 + }, + { + "days": 1.476389, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.477778, + "weight": 0.00010380755172621031 + }, + { + "days": 1.479167, + "weight": 4.5529627950092245e-05 + }, + { + "days": 1.48125, + "weight": 0.00042979968784887077 + }, + { + "days": 1.482639, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.495833, + "weight": 7.466858983815127e-05 + }, + { + "days": 1.498611, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.499306, + "weight": 0.0002130786588064317 + }, + { + "days": 1.504167, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.507639, + "weight": 9.470162613619187e-05 + }, + { + "days": 1.508333, + "weight": 9.288044101818817e-05 + }, + { + "days": 1.509028, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.509722, + "weight": 0.00010744992196221769 + }, + { + "days": 1.510417, + "weight": 3.46025172420701e-05 + }, + { + "days": 1.513194, + "weight": 0.00012019821778824352 + }, + { + "days": 1.513889, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.515278, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.518056, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.51875, + "weight": 4.5529627950092245e-05 + }, + { + "days": 1.519444, + "weight": 0.00012201940290624721 + }, + { + "days": 1.520139, + "weight": 0.0004935411669789999 + }, + { + "days": 1.521528, + "weight": 0.00011291347731622876 + }, + { + "days": 1.522222, + "weight": 0.0001748337713283542 + }, + { + "days": 1.523611, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.524306, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.527083, + "weight": 0.00013658888385027673 + }, + { + "days": 1.527778, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.528472, + "weight": 7.831096007415866e-05 + }, + { + "days": 1.529167, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.530556, + "weight": 4.7350813068095936e-05 + }, + { + "days": 1.53125, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.532639, + "weight": 0.00013476769873227304 + }, + { + "days": 1.534028, + "weight": 7.466858983815127e-05 + }, + { + "days": 1.534722, + "weight": 8.74168856641771e-05 + }, + { + "days": 1.535417, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.536111, + "weight": 7.831096007415866e-05 + }, + { + "days": 1.536806, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.538194, + "weight": 0.00045893864973692983 + }, + { + "days": 1.545833, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.550694, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.55625, + "weight": 4.5529627950092245e-05 + }, + { + "days": 1.558333, + "weight": 0.0004316208729668745 + }, + { + "days": 1.564583, + "weight": 3.8244887478077486e-05 + }, + { + "days": 1.565972, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.567361, + "weight": 5.645673865811438e-05 + }, + { + "days": 1.568056, + "weight": 4.5529627950092245e-05 + }, + { + "days": 1.570139, + "weight": 4.5529627950092245e-05 + }, + { + "days": 1.572917, + "weight": 0.00014933717967630255 + }, + { + "days": 1.574306, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.577778, + "weight": 4.7350813068095936e-05 + }, + { + "days": 1.578472, + "weight": 5.2814368422107004e-05 + }, + { + "days": 1.579167, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.580556, + "weight": 0.00015480073503031363 + }, + { + "days": 1.581944, + "weight": 0.0005062894628050258 + }, + { + "days": 1.582639, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.583333, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.585417, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.586806, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.5875, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.588194, + "weight": 0.0001602642903843247 + }, + { + "days": 1.590972, + "weight": 0.00010562873684421401 + }, + { + "days": 1.592361, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.593056, + "weight": 9.105925590018449e-05 + }, + { + "days": 1.594444, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.595833, + "weight": 0.0004134090217868376 + }, + { + "days": 1.596528, + "weight": 7.831096007415866e-05 + }, + { + "days": 1.597917, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.599306, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.6, + "weight": 3.46025172420701e-05 + }, + { + "days": 1.601389, + "weight": 3.096014700606273e-05 + }, + { + "days": 1.615278, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.621528, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.625, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.627083, + "weight": 3.8244887478077486e-05 + }, + { + "days": 1.634722, + "weight": 0.00010562873684421401 + }, + { + "days": 1.6375, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.639583, + "weight": 7.466858983815127e-05 + }, + { + "days": 1.640972, + "weight": 7.831096007415866e-05 + }, + { + "days": 1.641667, + "weight": 3.46025172420701e-05 + }, + { + "days": 1.642361, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.64375, + "weight": 0.00017119140109234684 + }, + { + "days": 1.644444, + "weight": 9.105925590018449e-05 + }, + { + "days": 1.645833, + "weight": 6.92050344841402e-05 + }, + { + "days": 1.646528, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.647917, + "weight": 0.00022400576951445383 + }, + { + "days": 1.650694, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.653472, + "weight": 0.00010198636660820662 + }, + { + "days": 1.654861, + "weight": 0.00022400576951445383 + }, + { + "days": 1.655556, + "weight": 0.00016754903085633945 + }, + { + "days": 1.65625, + "weight": 6.009910889412176e-05 + }, + { + "days": 1.656944, + "weight": 0.00012201940290624721 + }, + { + "days": 1.658333, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.659722, + "weight": 0.0004807928711529741 + }, + { + "days": 1.6625, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.668056, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.670833, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.671528, + "weight": 0.0001402312540862841 + }, + { + "days": 1.676389, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.684028, + "weight": 3.46025172420701e-05 + }, + { + "days": 1.689583, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.691667, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.69375, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.696528, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.697917, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.698611, + "weight": 3.8244887478077486e-05 + }, + { + "days": 1.699306, + "weight": 9.470162613619187e-05 + }, + { + "days": 1.700694, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.703472, + "weight": 0.00010744992196221769 + }, + { + "days": 1.704861, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.705556, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.709722, + "weight": 0.00019850917786240217 + }, + { + "days": 1.711806, + "weight": 0.00012201940290624721 + }, + { + "days": 1.7125, + "weight": 5.4635553540110695e-05 + }, + { + "days": 1.713889, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.714583, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.715278, + "weight": 3.46025172420701e-05 + }, + { + "days": 1.715972, + "weight": 0.00010380755172621031 + }, + { + "days": 1.716667, + "weight": 0.00019122443739038743 + }, + { + "days": 1.71875, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.720139, + "weight": 0.000792215526331605 + }, + { + "days": 1.721528, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.731944, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.735417, + "weight": 0.00011655584755223614 + }, + { + "days": 1.738889, + "weight": 7.466858983815127e-05 + }, + { + "days": 1.743056, + "weight": 3.8244887478077486e-05 + }, + { + "days": 1.744444, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.748611, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.757639, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.758333, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.760417, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.761111, + "weight": 8.74168856641771e-05 + }, + { + "days": 1.7625, + "weight": 0.00021672102904243907 + }, + { + "days": 1.763194, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.766667, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.772222, + "weight": 8.74168856641771e-05 + }, + { + "days": 1.773611, + "weight": 0.00014569480944029518 + }, + { + "days": 1.775, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.775694, + "weight": 0.00010198636660820662 + }, + { + "days": 1.777083, + "weight": 9.652281125419556e-05 + }, + { + "days": 1.777778, + "weight": 0.00010016518149020294 + }, + { + "days": 1.779167, + "weight": 9.834399637219924e-05 + }, + { + "days": 1.781944, + "weight": 0.00020033036298040588 + }, + { + "days": 1.782639, + "weight": 0.0006793020490153763 + }, + { + "days": 1.789583, + "weight": 0.00012748295826025828 + }, + { + "days": 1.810417, + "weight": 6.009910889412176e-05 + }, + { + "days": 1.813194, + "weight": 3.8244887478077486e-05 + }, + { + "days": 1.815278, + "weight": 4.006607259608118e-05 + }, + { + "days": 1.819444, + "weight": 0.00011109229219822507 + }, + { + "days": 1.820833, + "weight": 3.8244887478077486e-05 + }, + { + "days": 1.821528, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.823611, + "weight": 9.105925590018449e-05 + }, + { + "days": 1.825, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.825694, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.827083, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.828472, + "weight": 9.834399637219924e-05 + }, + { + "days": 1.829167, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.83125, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.831944, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.834028, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.834722, + "weight": 0.00011291347731622876 + }, + { + "days": 1.835417, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.836806, + "weight": 0.00020397273321641325 + }, + { + "days": 1.8375, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.838889, + "weight": 8.74168856641771e-05 + }, + { + "days": 1.839583, + "weight": 5.2814368422107004e-05 + }, + { + "days": 1.840278, + "weight": 9.288044101818817e-05 + }, + { + "days": 1.840972, + "weight": 0.000158443105266321 + }, + { + "days": 1.844444, + "weight": 0.0007047986406674279 + }, + { + "days": 1.845833, + "weight": 4.7350813068095936e-05 + }, + { + "days": 1.847917, + "weight": 8.377451542816972e-05 + }, + { + "days": 1.849306, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.85625, + "weight": 9.834399637219924e-05 + }, + { + "days": 1.857639, + "weight": 3.8244887478077486e-05 + }, + { + "days": 1.863889, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.867361, + "weight": 3.8244887478077486e-05 + }, + { + "days": 1.868056, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.872222, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.872917, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.878472, + "weight": 0.00011655584755223614 + }, + { + "days": 1.880556, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.88125, + "weight": 5.4635553540110695e-05 + }, + { + "days": 1.882639, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.883333, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.884722, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.885417, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.886806, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.888194, + "weight": 6.738384936613652e-05 + }, + { + "days": 1.890278, + "weight": 0.0002804625081725682 + }, + { + "days": 1.891667, + "weight": 0.00011291347731622876 + }, + { + "days": 1.894444, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.895833, + "weight": 9.105925590018449e-05 + }, + { + "days": 1.897222, + "weight": 7.831096007415866e-05 + }, + { + "days": 1.897917, + "weight": 0.0002804625081725682 + }, + { + "days": 1.898611, + "weight": 9.105925590018449e-05 + }, + { + "days": 1.899306, + "weight": 0.0001930456225083911 + }, + { + "days": 1.9, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.900694, + "weight": 0.0001384100689682804 + }, + { + "days": 1.906944, + "weight": 0.00011109229219822507 + }, + { + "days": 1.907639, + "weight": 0.0010927110708022138 + }, + { + "days": 1.908333, + "weight": 5.645673865811438e-05 + }, + { + "days": 1.909722, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.916667, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.921528, + "weight": 3.096014700606273e-05 + }, + { + "days": 1.922917, + "weight": 7.831096007415866e-05 + }, + { + "days": 1.923611, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.925694, + "weight": 3.46025172420701e-05 + }, + { + "days": 1.932639, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.933333, + "weight": 0.00013476769873227304 + }, + { + "days": 1.936111, + "weight": 4.5529627950092245e-05 + }, + { + "days": 1.939583, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.940278, + "weight": 0.00012748295826025828 + }, + { + "days": 1.941667, + "weight": 3.8244887478077486e-05 + }, + { + "days": 1.942361, + "weight": 0.00010744992196221769 + }, + { + "days": 1.943056, + "weight": 6.009910889412176e-05 + }, + { + "days": 1.945139, + "weight": 6.009910889412176e-05 + }, + { + "days": 1.945833, + "weight": 0.00011291347731622876 + }, + { + "days": 1.946528, + "weight": 0.00017847614156436158 + }, + { + "days": 1.947222, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.947917, + "weight": 5.4635553540110695e-05 + }, + { + "days": 1.949306, + "weight": 7.648977495615497e-05 + }, + { + "days": 1.95, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.950694, + "weight": 0.00010198636660820662 + }, + { + "days": 1.951389, + "weight": 0.00011109229219822507 + }, + { + "days": 1.954861, + "weight": 0.00023493288022247597 + }, + { + "days": 1.955556, + "weight": 4.188725771408486e-05 + }, + { + "days": 1.956944, + "weight": 0.0001602642903843247 + }, + { + "days": 1.958333, + "weight": 6.92050344841402e-05 + }, + { + "days": 1.959028, + "weight": 0.0004625810199729372 + }, + { + "days": 1.959722, + "weight": 0.00018758206715438003 + }, + { + "days": 1.961111, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.963194, + "weight": 3.278133212406642e-05 + }, + { + "days": 1.963889, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.964583, + "weight": 0.0002312905099864686 + }, + { + "days": 1.965972, + "weight": 0.0004571174646189261 + }, + { + "days": 1.966667, + "weight": 5.4635553540110695e-05 + }, + { + "days": 1.968056, + "weight": 5.2814368422107004e-05 + }, + { + "days": 1.970139, + "weight": 6.192029401212545e-05 + }, + { + "days": 1.970833, + "weight": 5.2814368422107004e-05 + }, + { + "days": 1.972917, + "weight": 0.00010562873684421401 + }, + { + "days": 1.975694, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.98125, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.981944, + "weight": 8.195333031016604e-05 + }, + { + "days": 1.982639, + "weight": 0.00020215154809840956 + }, + { + "days": 1.9875, + "weight": 0.00010562873684421401 + }, + { + "days": 1.988194, + "weight": 0.00011109229219822507 + }, + { + "days": 1.990972, + "weight": 6.738384936613652e-05 + }, + { + "days": 1.99375, + "weight": 0.00012019821778824352 + }, + { + "days": 1.997222, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.997917, + "weight": 7.10262196021439e-05 + }, + { + "days": 1.998611, + "weight": 0.0002804625081725682 + }, + { + "days": 1.999306, + "weight": 9.470162613619187e-05 + }, + { + "days": 2.001389, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.002083, + "weight": 0.00012930414337826196 + }, + { + "days": 2.004167, + "weight": 0.0004261573176128634 + }, + { + "days": 2.004861, + "weight": 0.00021854221416044278 + }, + { + "days": 2.005556, + "weight": 3.8244887478077486e-05 + }, + { + "days": 2.006944, + "weight": 4.006607259608118e-05 + }, + { + "days": 2.008333, + "weight": 9.105925590018449e-05 + }, + { + "days": 2.009028, + "weight": 0.00021489984392443538 + }, + { + "days": 2.009722, + "weight": 0.00018576088203637635 + }, + { + "days": 2.010417, + "weight": 0.00015297954991230994 + }, + { + "days": 2.013194, + "weight": 4.5529627950092245e-05 + }, + { + "days": 2.014583, + "weight": 7.648977495615497e-05 + }, + { + "days": 2.015278, + "weight": 6.009910889412176e-05 + }, + { + "days": 2.015972, + "weight": 0.00029138961888059035 + }, + { + "days": 2.018056, + "weight": 6.556266424813284e-05 + }, + { + "days": 2.01875, + "weight": 0.00010927110708022139 + }, + { + "days": 2.020139, + "weight": 0.00013476769873227304 + }, + { + "days": 2.020833, + "weight": 0.00038062768966277115 + }, + { + "days": 2.023611, + "weight": 0.00013476769873227304 + }, + { + "days": 2.024306, + "weight": 6.738384936613652e-05 + }, + { + "days": 2.025694, + "weight": 0.0005700309419351549 + }, + { + "days": 2.03125, + "weight": 0.00034056161706669 + }, + { + "days": 2.031944, + "weight": 6.374147913012914e-05 + }, + { + "days": 2.034028, + "weight": 0.00010380755172621031 + }, + { + "days": 2.041667, + "weight": 7.10262196021439e-05 + }, + { + "days": 2.042361, + "weight": 0.00010380755172621031 + }, + { + "days": 2.044444, + "weight": 5.4635553540110695e-05 + }, + { + "days": 2.045139, + "weight": 5.645673865811438e-05 + }, + { + "days": 2.046528, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.048611, + "weight": 5.2814368422107004e-05 + }, + { + "days": 2.049306, + "weight": 8.195333031016604e-05 + }, + { + "days": 2.052778, + "weight": 9.652281125419556e-05 + }, + { + "days": 2.053472, + "weight": 5.4635553540110695e-05 + }, + { + "days": 2.054167, + "weight": 4.006607259608118e-05 + }, + { + "days": 2.056944, + "weight": 0.00011109229219822507 + }, + { + "days": 2.057639, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.059028, + "weight": 4.006607259608118e-05 + }, + { + "days": 2.059722, + "weight": 8.195333031016604e-05 + }, + { + "days": 2.063889, + "weight": 0.00012930414337826196 + }, + { + "days": 2.065278, + "weight": 6.738384936613652e-05 + }, + { + "days": 2.065972, + "weight": 4.006607259608118e-05 + }, + { + "days": 2.066667, + "weight": 7.831096007415866e-05 + }, + { + "days": 2.068056, + "weight": 0.00012019821778824352 + }, + { + "days": 2.070833, + "weight": 0.00018393969691837266 + }, + { + "days": 2.071528, + "weight": 7.10262196021439e-05 + }, + { + "days": 2.072917, + "weight": 8.74168856641771e-05 + }, + { + "days": 2.073611, + "weight": 5.4635553540110695e-05 + }, + { + "days": 2.075, + "weight": 0.00018393969691837266 + }, + { + "days": 2.077083, + "weight": 0.00013112532849626567 + }, + { + "days": 2.078472, + "weight": 0.00011109229219822507 + }, + { + "days": 2.079861, + "weight": 9.470162613619187e-05 + }, + { + "days": 2.080556, + "weight": 4.917199818609962e-05 + }, + { + "days": 2.082639, + "weight": 7.831096007415866e-05 + }, + { + "days": 2.083333, + "weight": 3.096014700606273e-05 + }, + { + "days": 2.086806, + "weight": 0.00040066072596081176 + }, + { + "days": 2.093056, + "weight": 0.0005645673865811438 + }, + { + "days": 2.095139, + "weight": 3.8244887478077486e-05 + }, + { + "days": 2.108333, + "weight": 8.74168856641771e-05 + }, + { + "days": 2.110417, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.115972, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.117361, + "weight": 3.096014700606273e-05 + }, + { + "days": 2.11875, + "weight": 0.0001930456225083911 + }, + { + "days": 2.120833, + "weight": 6.374147913012914e-05 + }, + { + "days": 2.122917, + "weight": 3.278133212406642e-05 + }, + { + "days": 2.125, + "weight": 4.917199818609962e-05 + }, + { + "days": 2.126389, + "weight": 3.8244887478077486e-05 + }, + { + "days": 2.127778, + "weight": 7.284740472014759e-05 + }, + { + "days": 2.129861, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.130556, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.132639, + "weight": 3.278133212406642e-05 + }, + { + "days": 2.134028, + "weight": 6.556266424813284e-05 + }, + { + "days": 2.136111, + "weight": 0.00011655584755223614 + }, + { + "days": 2.1375, + "weight": 0.00016754903085633945 + }, + { + "days": 2.138194, + "weight": 9.470162613619187e-05 + }, + { + "days": 2.138889, + "weight": 4.7350813068095936e-05 + }, + { + "days": 2.140278, + "weight": 0.00017301258621035053 + }, + { + "days": 2.140972, + "weight": 5.2814368422107004e-05 + }, + { + "days": 2.145139, + "weight": 4.7350813068095936e-05 + }, + { + "days": 2.15, + "weight": 0.00013658888385027673 + }, + { + "days": 2.152778, + "weight": 3.278133212406642e-05 + }, + { + "days": 2.154861, + "weight": 0.00020761510345242062 + }, + { + "days": 2.159028, + "weight": 0.00011291347731622876 + }, + { + "days": 2.164583, + "weight": 4.006607259608118e-05 + }, + { + "days": 2.165972, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.168056, + "weight": 3.46025172420701e-05 + }, + { + "days": 2.178472, + "weight": 3.096014700606273e-05 + }, + { + "days": 2.180556, + "weight": 6.556266424813284e-05 + }, + { + "days": 2.181944, + "weight": 0.00012019821778824352 + }, + { + "days": 2.182639, + "weight": 6.009910889412176e-05 + }, + { + "days": 2.184722, + "weight": 0.00012201940290624721 + }, + { + "days": 2.188194, + "weight": 3.8244887478077486e-05 + }, + { + "days": 2.189583, + "weight": 6.738384936613652e-05 + }, + { + "days": 2.190972, + "weight": 7.284740472014759e-05 + }, + { + "days": 2.191667, + "weight": 4.006607259608118e-05 + }, + { + "days": 2.195139, + "weight": 6.92050344841402e-05 + }, + { + "days": 2.196528, + "weight": 3.278133212406642e-05 + }, + { + "days": 2.197222, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.197917, + "weight": 4.006607259608118e-05 + }, + { + "days": 2.198611, + "weight": 7.466858983815127e-05 + }, + { + "days": 2.199306, + "weight": 6.192029401212545e-05 + }, + { + "days": 2.2, + "weight": 7.10262196021439e-05 + }, + { + "days": 2.204167, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.206944, + "weight": 0.00015480073503031363 + }, + { + "days": 2.210417, + "weight": 0.00028956843376258664 + }, + { + "days": 2.211806, + "weight": 3.096014700606273e-05 + }, + { + "days": 2.220833, + "weight": 0.00014751599455829886 + }, + { + "days": 2.238889, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.241667, + "weight": 5.2814368422107004e-05 + }, + { + "days": 2.24375, + "weight": 7.466858983815127e-05 + }, + { + "days": 2.245139, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.246528, + "weight": 5.645673865811438e-05 + }, + { + "days": 2.247917, + "weight": 0.00010198636660820662 + }, + { + "days": 2.250694, + "weight": 6.556266424813284e-05 + }, + { + "days": 2.252778, + "weight": 5.827792377611807e-05 + }, + { + "days": 2.253472, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.254167, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.25625, + "weight": 8.74168856641771e-05 + }, + { + "days": 2.256944, + "weight": 0.0001420524392042878 + }, + { + "days": 2.258333, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.259028, + "weight": 5.645673865811438e-05 + }, + { + "days": 2.261806, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.266667, + "weight": 0.00010744992196221769 + }, + { + "days": 2.269444, + "weight": 0.00024039643557648705 + }, + { + "days": 2.272222, + "weight": 0.00020033036298040588 + }, + { + "days": 2.299306, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.302778, + "weight": 6.192029401212545e-05 + }, + { + "days": 2.309722, + "weight": 4.7350813068095936e-05 + }, + { + "days": 2.311806, + "weight": 7.648977495615497e-05 + }, + { + "days": 2.3125, + "weight": 6.374147913012914e-05 + }, + { + "days": 2.313194, + "weight": 3.46025172420701e-05 + }, + { + "days": 2.314583, + "weight": 6.738384936613652e-05 + }, + { + "days": 2.315972, + "weight": 6.738384936613652e-05 + }, + { + "days": 2.318056, + "weight": 0.00013658888385027673 + }, + { + "days": 2.31875, + "weight": 5.827792377611807e-05 + }, + { + "days": 2.325694, + "weight": 0.00010380755172621031 + }, + { + "days": 2.330556, + "weight": 0.00027135658258254974 + }, + { + "days": 2.33125, + "weight": 0.00013476769873227304 + }, + { + "days": 2.336111, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.338194, + "weight": 0.00013112532849626567 + }, + { + "days": 2.360417, + "weight": 5.827792377611807e-05 + }, + { + "days": 2.36875, + "weight": 5.2814368422107004e-05 + }, + { + "days": 2.373611, + "weight": 0.00011291347731622876 + }, + { + "days": 2.375, + "weight": 3.46025172420701e-05 + }, + { + "days": 2.376389, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.379167, + "weight": 8.377451542816972e-05 + }, + { + "days": 2.379861, + "weight": 3.46025172420701e-05 + }, + { + "days": 2.384722, + "weight": 0.00018393969691837266 + }, + { + "days": 2.385417, + "weight": 5.2814368422107004e-05 + }, + { + "days": 2.386111, + "weight": 0.000287747248644583 + }, + { + "days": 2.392361, + "weight": 0.00015480073503031363 + }, + { + "days": 2.39375, + "weight": 3.278133212406642e-05 + }, + { + "days": 2.397222, + "weight": 8.559570054617342e-05 + }, + { + "days": 2.421528, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.43125, + "weight": 8.559570054617342e-05 + }, + { + "days": 2.432639, + "weight": 6.738384936613652e-05 + }, + { + "days": 2.434722, + "weight": 8.74168856641771e-05 + }, + { + "days": 2.435417, + "weight": 3.096014700606273e-05 + }, + { + "days": 2.436806, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.4375, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.440278, + "weight": 0.00019122443739038743 + }, + { + "days": 2.44375, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.447917, + "weight": 0.00013294651361426936 + }, + { + "days": 2.448611, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.450694, + "weight": 0.00029685317423460144 + }, + { + "days": 2.451389, + "weight": 6.192029401212545e-05 + }, + { + "days": 2.458333, + "weight": 0.00015115836479430626 + }, + { + "days": 2.478472, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.49375, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.495139, + "weight": 3.278133212406642e-05 + }, + { + "days": 2.495833, + "weight": 8.013214519216235e-05 + }, + { + "days": 2.496528, + "weight": 6.92050344841402e-05 + }, + { + "days": 2.499306, + "weight": 0.00029685317423460144 + }, + { + "days": 2.501389, + "weight": 4.5529627950092245e-05 + }, + { + "days": 2.505556, + "weight": 0.00039883954084280805 + }, + { + "days": 2.509722, + "weight": 7.10262196021439e-05 + }, + { + "days": 2.511111, + "weight": 5.4635553540110695e-05 + }, + { + "days": 2.5125, + "weight": 0.00015297954991230994 + }, + { + "days": 2.513194, + "weight": 4.917199818609962e-05 + }, + { + "days": 2.513889, + "weight": 0.00011655584755223614 + }, + { + "days": 2.532639, + "weight": 4.006607259608118e-05 + }, + { + "days": 2.538194, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.552083, + "weight": 3.278133212406642e-05 + }, + { + "days": 2.554167, + "weight": 5.645673865811438e-05 + }, + { + "days": 2.557639, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.561111, + "weight": 3.46025172420701e-05 + }, + { + "days": 2.5625, + "weight": 6.556266424813284e-05 + }, + { + "days": 2.565278, + "weight": 7.831096007415866e-05 + }, + { + "days": 2.565972, + "weight": 0.0005099318330410331 + }, + { + "days": 2.567361, + "weight": 0.00022946932486846491 + }, + { + "days": 2.568056, + "weight": 6.009910889412176e-05 + }, + { + "days": 2.570139, + "weight": 8.923807078218079e-05 + }, + { + "days": 2.570833, + "weight": 0.00021672102904243907 + }, + { + "days": 2.578472, + "weight": 0.0001620854755023284 + }, + { + "days": 2.602083, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.620139, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.621528, + "weight": 0.00019850917786240217 + }, + { + "days": 2.628472, + "weight": 0.00020215154809840956 + }, + { + "days": 2.629167, + "weight": 0.00010198636660820662 + }, + { + "days": 2.629861, + "weight": 0.00013294651361426936 + }, + { + "days": 2.63125, + "weight": 6.92050344841402e-05 + }, + { + "days": 2.632639, + "weight": 0.00020033036298040588 + }, + { + "days": 2.633333, + "weight": 0.0001766549564463579 + }, + { + "days": 2.636806, + "weight": 0.0006246664954752656 + }, + { + "days": 2.672917, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.674306, + "weight": 6.556266424813284e-05 + }, + { + "days": 2.680556, + "weight": 0.0003059590998246199 + }, + { + "days": 2.684722, + "weight": 0.0001238405880242509 + }, + { + "days": 2.686806, + "weight": 9.105925590018449e-05 + }, + { + "days": 2.688194, + "weight": 0.0005408919800470958 + }, + { + "days": 2.689583, + "weight": 6.374147913012914e-05 + }, + { + "days": 2.690278, + "weight": 0.00014751599455829886 + }, + { + "days": 2.69375, + "weight": 4.006607259608118e-05 + }, + { + "days": 2.694444, + "weight": 0.00010198636660820662 + }, + { + "days": 2.698611, + "weight": 0.0001438736243222915 + }, + { + "days": 2.700694, + "weight": 4.5529627950092245e-05 + }, + { + "days": 2.713889, + "weight": 3.096014700606273e-05 + }, + { + "days": 2.729861, + "weight": 6.192029401212545e-05 + }, + { + "days": 2.731944, + "weight": 3.096014700606273e-05 + }, + { + "days": 2.739583, + "weight": 0.00022582695463245752 + }, + { + "days": 2.747222, + "weight": 0.0007903943412136014 + }, + { + "days": 2.748611, + "weight": 8.013214519216235e-05 + }, + { + "days": 2.75, + "weight": 9.105925590018449e-05 + }, + { + "days": 2.750694, + "weight": 5.827792377611807e-05 + }, + { + "days": 2.751389, + "weight": 6.009910889412176e-05 + }, + { + "days": 2.754861, + "weight": 0.00012201940290624721 + }, + { + "days": 2.75625, + "weight": 0.00019850917786240217 + }, + { + "days": 2.757639, + "weight": 3.278133212406642e-05 + }, + { + "days": 2.780556, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.793056, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.794444, + "weight": 5.645673865811438e-05 + }, + { + "days": 2.804861, + "weight": 7.466858983815127e-05 + }, + { + "days": 2.805556, + "weight": 3.46025172420701e-05 + }, + { + "days": 2.80625, + "weight": 0.0014350938729869075 + }, + { + "days": 2.807639, + "weight": 9.652281125419556e-05 + }, + { + "days": 2.809028, + "weight": 3.278133212406642e-05 + }, + { + "days": 2.813889, + "weight": 0.0004134090217868376 + }, + { + "days": 2.816667, + "weight": 9.652281125419556e-05 + }, + { + "days": 2.822222, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.848611, + "weight": 6.374147913012914e-05 + }, + { + "days": 2.849306, + "weight": 6.374147913012914e-05 + }, + { + "days": 2.859028, + "weight": 3.46025172420701e-05 + }, + { + "days": 2.864583, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.867361, + "weight": 8.377451542816972e-05 + }, + { + "days": 2.868056, + "weight": 5.2814368422107004e-05 + }, + { + "days": 2.870833, + "weight": 8.559570054617342e-05 + }, + { + "days": 2.871528, + "weight": 0.0001948668076263948 + }, + { + "days": 2.873611, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.875, + "weight": 4.006607259608118e-05 + }, + { + "days": 2.875694, + "weight": 0.0003296345063586678 + }, + { + "days": 2.880556, + "weight": 0.006876795005581932 + }, + { + "days": 2.882639, + "weight": 0.0001602642903843247 + }, + { + "days": 2.890972, + "weight": 7.466858983815127e-05 + }, + { + "days": 2.895833, + "weight": 6.374147913012914e-05 + }, + { + "days": 2.913194, + "weight": 0.00011473466243423246 + }, + { + "days": 2.914583, + "weight": 0.0001766549564463579 + }, + { + "days": 2.91875, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.919444, + "weight": 5.645673865811438e-05 + }, + { + "days": 2.922917, + "weight": 0.00018940325227238374 + }, + { + "days": 2.925694, + "weight": 0.0003369192468306826 + }, + { + "days": 2.929167, + "weight": 0.00028956843376258664 + }, + { + "days": 2.934028, + "weight": 0.0002677142123465424 + }, + { + "days": 2.9375, + "weight": 7.284740472014759e-05 + }, + { + "days": 2.940972, + "weight": 0.00015115836479430626 + }, + { + "days": 2.941667, + "weight": 0.0009761552232499777 + }, + { + "days": 2.942361, + "weight": 0.004334420580848782 + }, + { + "days": 2.95, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.954167, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.957639, + "weight": 8.923807078218079e-05 + }, + { + "days": 2.959028, + "weight": 4.7350813068095936e-05 + }, + { + "days": 2.959722, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.960417, + "weight": 4.188725771408486e-05 + }, + { + "days": 2.965278, + "weight": 3.46025172420701e-05 + }, + { + "days": 2.965972, + "weight": 4.7350813068095936e-05 + }, + { + "days": 2.968056, + "weight": 0.0003879124301347859 + }, + { + "days": 2.969444, + "weight": 5.4635553540110695e-05 + }, + { + "days": 2.971528, + "weight": 3.8244887478077486e-05 + }, + { + "days": 2.972222, + "weight": 0.00046622339020894457 + }, + { + "days": 2.972917, + "weight": 4.7350813068095936e-05 + }, + { + "days": 2.974306, + "weight": 0.00012201940290624721 + }, + { + "days": 2.976389, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.977083, + "weight": 6.192029401212545e-05 + }, + { + "days": 2.978472, + "weight": 7.10262196021439e-05 + }, + { + "days": 2.982639, + "weight": 7.10262196021439e-05 + }, + { + "days": 2.984722, + "weight": 0.0002677142123465424 + }, + { + "days": 2.985417, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.986806, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.9875, + "weight": 0.0004971835372150073 + }, + { + "days": 2.990972, + "weight": 0.0008268180435736752 + }, + { + "days": 2.993056, + "weight": 0.0006246664954752656 + }, + { + "days": 2.995139, + "weight": 0.0005791368675251733 + }, + { + "days": 2.997917, + "weight": 0.0002458599909304981 + }, + { + "days": 3.0, + "weight": 0.0004680445753269483 + }, + { + "days": 3.008333, + "weight": 0.002460421094422985 + }, + { + "days": 3.011111, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.015278, + "weight": 8.923807078218079e-05 + }, + { + "days": 3.017361, + "weight": 6.192029401212545e-05 + }, + { + "days": 3.030556, + "weight": 7.10262196021439e-05 + }, + { + "days": 3.031944, + "weight": 5.2814368422107004e-05 + }, + { + "days": 3.032639, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.033333, + "weight": 0.00018393969691837266 + }, + { + "days": 3.036111, + "weight": 0.00014569480944029518 + }, + { + "days": 3.0375, + "weight": 5.827792377611807e-05 + }, + { + "days": 3.038194, + "weight": 0.0003223497658866531 + }, + { + "days": 3.042361, + "weight": 0.0006665537531893505 + }, + { + "days": 3.04375, + "weight": 8.923807078218079e-05 + }, + { + "days": 3.048611, + "weight": 0.0004971835372150073 + }, + { + "days": 3.049306, + "weight": 0.0001420524392042878 + }, + { + "days": 3.050694, + "weight": 0.0003478463575387047 + }, + { + "days": 3.052083, + "weight": 3.278133212406642e-05 + }, + { + "days": 3.055556, + "weight": 7.10262196021439e-05 + }, + { + "days": 3.056944, + "weight": 9.470162613619187e-05 + }, + { + "days": 3.059722, + "weight": 0.00041523020690484124 + }, + { + "days": 3.061111, + "weight": 0.00023493288022247597 + }, + { + "days": 3.090972, + "weight": 9.470162613619187e-05 + }, + { + "days": 3.095139, + "weight": 3.46025172420701e-05 + }, + { + "days": 3.098611, + "weight": 3.8244887478077486e-05 + }, + { + "days": 3.104167, + "weight": 5.2814368422107004e-05 + }, + { + "days": 3.105556, + "weight": 4.370844283208855e-05 + }, + { + "days": 3.106944, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.107639, + "weight": 0.0001930456225083911 + }, + { + "days": 3.109028, + "weight": 6.009910889412176e-05 + }, + { + "days": 3.1125, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.114583, + "weight": 0.00015297954991230994 + }, + { + "days": 3.115278, + "weight": 7.284740472014759e-05 + }, + { + "days": 3.115972, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.116667, + "weight": 7.648977495615497e-05 + }, + { + "days": 3.123611, + "weight": 7.648977495615497e-05 + }, + { + "days": 3.138194, + "weight": 3.46025172420701e-05 + }, + { + "days": 3.15, + "weight": 3.8244887478077486e-05 + }, + { + "days": 3.156944, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.157639, + "weight": 4.7350813068095936e-05 + }, + { + "days": 3.165278, + "weight": 8.377451542816972e-05 + }, + { + "days": 3.169444, + "weight": 5.645673865811438e-05 + }, + { + "days": 3.170139, + "weight": 4.7350813068095936e-05 + }, + { + "days": 3.174306, + "weight": 3.8244887478077486e-05 + }, + { + "days": 3.175, + "weight": 8.195333031016604e-05 + }, + { + "days": 3.177778, + "weight": 3.278133212406642e-05 + }, + { + "days": 3.18125, + "weight": 0.0001402312540862841 + }, + { + "days": 3.213194, + "weight": 0.00018758206715438003 + }, + { + "days": 3.224306, + "weight": 0.00011109229219822507 + }, + { + "days": 3.23125, + "weight": 6.192029401212545e-05 + }, + { + "days": 3.233333, + "weight": 3.8244887478077486e-05 + }, + { + "days": 3.234028, + "weight": 5.4635553540110695e-05 + }, + { + "days": 3.235417, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.236111, + "weight": 3.278133212406642e-05 + }, + { + "days": 3.275, + "weight": 5.827792377611807e-05 + }, + { + "days": 3.2875, + "weight": 0.00011109229219822507 + }, + { + "days": 3.291667, + "weight": 3.46025172420701e-05 + }, + { + "days": 3.293056, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.297917, + "weight": 3.278133212406642e-05 + }, + { + "days": 3.300694, + "weight": 0.00013294651361426936 + }, + { + "days": 3.343056, + "weight": 6.738384936613652e-05 + }, + { + "days": 3.347222, + "weight": 5.4635553540110695e-05 + }, + { + "days": 3.349306, + "weight": 5.827792377611807e-05 + }, + { + "days": 3.359028, + "weight": 3.8244887478077486e-05 + }, + { + "days": 3.404167, + "weight": 4.188725771408486e-05 + }, + { + "days": 3.406944, + "weight": 3.46025172420701e-05 + }, + { + "days": 3.409028, + "weight": 4.006607259608118e-05 + }, + { + "days": 3.417361, + "weight": 0.00010016518149020294 + }, + { + "days": 3.419444, + "weight": 5.2814368422107004e-05 + }, + { + "days": 3.478472, + "weight": 5.099318330410331e-05 + }, + { + "days": 3.479167, + "weight": 3.8244887478077486e-05 + }, + { + "days": 3.485417, + "weight": 4.917199818609962e-05 + }, + { + "days": 3.524306, + "weight": 3.46025172420701e-05 + }, + { + "days": 3.533333, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.544444, + "weight": 5.4635553540110695e-05 + }, + { + "days": 3.545139, + "weight": 9.652281125419556e-05 + }, + { + "days": 3.590278, + "weight": 4.5529627950092245e-05 + }, + { + "days": 3.611111, + "weight": 0.00010016518149020294 + }, + { + "days": 3.660417, + "weight": 4.7350813068095936e-05 + }, + { + "days": 3.670139, + "weight": 0.00018211851180036898 + }, + { + "days": 3.719444, + "weight": 6.009910889412176e-05 + }, + { + "days": 3.723611, + "weight": 3.46025172420701e-05 + }, + { + "days": 3.727778, + "weight": 0.00021489984392443538 + }, + { + "days": 3.757639, + "weight": 4.188725771408486e-05 + }, + { + "days": 3.767361, + "weight": 4.7350813068095936e-05 + }, + { + "days": 3.779167, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.782639, + "weight": 3.278133212406642e-05 + }, + { + "days": 3.784722, + "weight": 4.7350813068095936e-05 + }, + { + "days": 3.786111, + "weight": 0.00024221762069449073 + }, + { + "days": 3.798611, + "weight": 3.096014700606273e-05 + }, + { + "days": 3.804861, + "weight": 3.46025172420701e-05 + }, + { + "days": 3.809028, + "weight": 4.917199818609962e-05 + }, + { + "days": 3.822222, + "weight": 5.2814368422107004e-05 + }, + { + "days": 3.831944, + "weight": 4.5529627950092245e-05 + }, + { + "days": 3.839583, + "weight": 3.46025172420701e-05 + }, + { + "days": 3.840278, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.849306, + "weight": 0.00047168694556295565 + }, + { + "days": 3.857639, + "weight": 5.099318330410331e-05 + }, + { + "days": 3.86875, + "weight": 4.006607259608118e-05 + }, + { + "days": 3.884028, + "weight": 4.917199818609962e-05 + }, + { + "days": 3.89375, + "weight": 5.099318330410331e-05 + }, + { + "days": 3.898611, + "weight": 0.0009233408548278707 + }, + { + "days": 3.899306, + "weight": 3.46025172420701e-05 + }, + { + "days": 3.927083, + "weight": 9.105925590018449e-05 + }, + { + "days": 3.930556, + "weight": 6.92050344841402e-05 + }, + { + "days": 3.931944, + "weight": 0.0001948668076263948 + }, + { + "days": 3.932639, + "weight": 6.192029401212545e-05 + }, + { + "days": 3.943056, + "weight": 7.466858983815127e-05 + }, + { + "days": 3.94375, + "weight": 4.006607259608118e-05 + }, + { + "days": 3.945833, + "weight": 6.92050344841402e-05 + }, + { + "days": 3.952083, + "weight": 3.8244887478077486e-05 + }, + { + "days": 3.952778, + "weight": 0.0001238405880242509 + }, + { + "days": 3.953472, + "weight": 5.645673865811438e-05 + }, + { + "days": 3.95625, + "weight": 4.370844283208855e-05 + }, + { + "days": 3.959722, + "weight": 0.00013476769873227304 + }, + { + "days": 3.961111, + "weight": 4.370844283208855e-05 + }, + { + "days": 3.963889, + "weight": 8.923807078218079e-05 + }, + { + "days": 3.965278, + "weight": 0.00044072679855689293 + }, + { + "days": 3.968056, + "weight": 4.006607259608118e-05 + }, + { + "days": 3.984028, + "weight": 4.006607259608118e-05 + }, + { + "days": 3.99375, + "weight": 0.0001748337713283542 + }, + { + "days": 3.995833, + "weight": 9.288044101818817e-05 + }, + { + "days": 3.996528, + "weight": 6.738384936613652e-05 + }, + { + "days": 3.998611, + "weight": 8.923807078218079e-05 + }, + { + "days": 4.001389, + "weight": 0.00021672102904243907 + }, + { + "days": 4.002778, + "weight": 4.006607259608118e-05 + }, + { + "days": 4.004167, + "weight": 5.099318330410331e-05 + }, + { + "days": 4.00625, + "weight": 4.006607259608118e-05 + }, + { + "days": 4.007639, + "weight": 7.831096007415866e-05 + }, + { + "days": 4.008333, + "weight": 3.6423702360073794e-05 + }, + { + "days": 4.011806, + "weight": 0.00014569480944029518 + }, + { + "days": 4.015278, + "weight": 7.10262196021439e-05 + }, + { + "days": 4.018056, + "weight": 6.192029401212545e-05 + }, + { + "days": 4.01875, + "weight": 7.10262196021439e-05 + }, + { + "days": 4.019444, + "weight": 0.00024403880581249442 + }, + { + "days": 4.020833, + "weight": 0.0001256617731422546 + }, + { + "days": 4.021528, + "weight": 5.099318330410331e-05 + }, + { + "days": 4.025, + "weight": 0.0001948668076263948 + }, + { + "days": 4.029861, + "weight": 3.8244887478077486e-05 + }, + { + "days": 4.035417, + "weight": 4.188725771408486e-05 + }, + { + "days": 4.047917, + "weight": 6.009910889412176e-05 + }, + { + "days": 4.054167, + "weight": 4.006607259608118e-05 + }, + { + "days": 4.059722, + "weight": 5.827792377611807e-05 + }, + { + "days": 4.063194, + "weight": 9.105925590018449e-05 + }, + { + "days": 4.069444, + "weight": 3.096014700606273e-05 + }, + { + "days": 4.070139, + "weight": 7.466858983815127e-05 + }, + { + "days": 4.074306, + "weight": 9.105925590018449e-05 + }, + { + "days": 4.078472, + "weight": 3.46025172420701e-05 + }, + { + "days": 4.079861, + "weight": 4.370844283208855e-05 + }, + { + "days": 4.080556, + "weight": 3.46025172420701e-05 + }, + { + "days": 4.082639, + "weight": 0.00013658888385027673 + }, + { + "days": 4.095833, + "weight": 3.278133212406642e-05 + }, + { + "days": 4.11875, + "weight": 3.46025172420701e-05 + }, + { + "days": 4.127083, + "weight": 6.556266424813284e-05 + }, + { + "days": 4.129167, + "weight": 4.370844283208855e-05 + }, + { + "days": 4.131944, + "weight": 5.645673865811438e-05 + }, + { + "days": 4.1375, + "weight": 4.5529627950092245e-05 + }, + { + "days": 4.148611, + "weight": 9.470162613619187e-05 + }, + { + "days": 4.181944, + "weight": 5.2814368422107004e-05 + }, + { + "days": 4.209028, + "weight": 3.46025172420701e-05 + }, + { + "days": 4.211806, + "weight": 3.8244887478077486e-05 + }, + { + "days": 4.23125, + "weight": 5.4635553540110695e-05 + }, + { + "days": 4.268056, + "weight": 3.278133212406642e-05 + }, + { + "days": 4.273611, + "weight": 3.6423702360073794e-05 + }, + { + "days": 4.305556, + "weight": 3.278133212406642e-05 + }, + { + "days": 4.334722, + "weight": 0.00010744992196221769 + }, + { + "days": 4.438889, + "weight": 4.006607259608118e-05 + }, + { + "days": 4.450694, + "weight": 3.096014700606273e-05 + }, + { + "days": 4.467361, + "weight": 3.6423702360073794e-05 + }, + { + "days": 4.619444, + "weight": 5.4635553540110695e-05 + }, + { + "days": 4.731944, + "weight": 3.46025172420701e-05 + }, + { + "days": 4.760417, + "weight": 3.6423702360073794e-05 + }, + { + "days": 4.770833, + "weight": 0.00022400576951445383 + }, + { + "days": 4.801389, + "weight": 3.46025172420701e-05 + }, + { + "days": 4.827083, + "weight": 0.00017119140109234684 + }, + { + "days": 4.884722, + "weight": 0.00029138961888059035 + }, + { + "days": 4.886806, + "weight": 3.278133212406642e-05 + }, + { + "days": 4.900694, + "weight": 4.006607259608118e-05 + }, + { + "days": 4.940278, + "weight": 3.278133212406642e-05 + }, + { + "days": 4.942361, + "weight": 7.648977495615497e-05 + }, + { + "days": 4.95, + "weight": 0.00011109229219822507 + }, + { + "days": 4.951389, + "weight": 0.00029138961888059035 + }, + { + "days": 4.952083, + "weight": 3.278133212406642e-05 + }, + { + "days": 4.952778, + "weight": 3.278133212406642e-05 + }, + { + "days": 4.963889, + "weight": 4.7350813068095936e-05 + }, + { + "days": 4.970139, + "weight": 0.00018758206715438003 + }, + { + "days": 4.978472, + "weight": 4.006607259608118e-05 + }, + { + "days": 4.9875, + "weight": 7.466858983815127e-05 + }, + { + "days": 4.99375, + "weight": 3.6423702360073794e-05 + }, + { + "days": 5.0, + "weight": 3.096014700606273e-05 + }, + { + "days": 5.00625, + "weight": 3.8244887478077486e-05 + }, + { + "days": 5.007639, + "weight": 4.917199818609962e-05 + }, + { + "days": 5.011806, + "weight": 4.917199818609962e-05 + }, + { + "days": 5.016667, + "weight": 4.7350813068095936e-05 + }, + { + "days": 5.018056, + "weight": 0.00011837703267023983 + }, + { + "days": 5.019444, + "weight": 7.831096007415866e-05 + }, + { + "days": 5.027778, + "weight": 4.006607259608118e-05 + }, + { + "days": 5.03125, + "weight": 3.6423702360073794e-05 + }, + { + "days": 5.054861, + "weight": 8.195333031016604e-05 + }, + { + "days": 5.078472, + "weight": 5.645673865811438e-05 + }, + { + "days": 5.086111, + "weight": 4.006607259608118e-05 + }, + { + "days": 5.095833, + "weight": 4.370844283208855e-05 + }, + { + "days": 5.097917, + "weight": 3.278133212406642e-05 + }, + { + "days": 5.103472, + "weight": 7.10262196021439e-05 + }, + { + "days": 5.114583, + "weight": 3.8244887478077486e-05 + }, + { + "days": 5.13125, + "weight": 3.6423702360073794e-05 + }, + { + "days": 5.771528, + "weight": 7.10262196021439e-05 + }, + { + "days": 5.838194, + "weight": 0.00010562873684421401 + }, + { + "days": 5.891667, + "weight": 4.370844283208855e-05 + }, + { + "days": 5.91875, + "weight": 0.00016754903085633945 + }, + { + "days": 5.936111, + "weight": 5.4635553540110695e-05 + }, + { + "days": 5.947917, + "weight": 3.46025172420701e-05 + }, + { + "days": 5.986111, + "weight": 8.74168856641771e-05 + }, + { + "days": 5.997222, + "weight": 4.370844283208855e-05 + }, + { + "days": 5.998611, + "weight": 3.6423702360073794e-05 + }, + { + "days": 6.002778, + "weight": 5.4635553540110695e-05 + }, + { + "days": 6.00625, + "weight": 4.917199818609962e-05 + }, + { + "days": 6.017361, + "weight": 4.5529627950092245e-05 + }, + { + "days": 6.04375, + "weight": 6.374147913012914e-05 + }, + { + "days": 6.124306, + "weight": 4.370844283208855e-05 + }, + { + "days": 6.765972, + "weight": 4.188725771408486e-05 + }, + { + "days": 6.844444, + "weight": 7.648977495615497e-05 + }, + { + "days": 6.902083, + "weight": 7.831096007415866e-05 + }, + { + "days": 6.947917, + "weight": 3.6423702360073794e-05 + }, + { + "days": 6.963194, + "weight": 8.195333031016604e-05 + }, + { + "days": 6.970833, + "weight": 4.006607259608118e-05 + }, + { + "days": 7.010417, + "weight": 6.374147913012914e-05 + }, + { + "days": 7.017361, + "weight": 5.2814368422107004e-05 + }, + { + "days": 7.018056, + "weight": 3.278133212406642e-05 + }, + { + "days": 7.051389, + "weight": 3.8244887478077486e-05 + }, + { + "days": 7.965278, + "weight": 3.6423702360073794e-05 + }, + { + "days": 9.828472, + "weight": 0.00010016518149020294 + }, + { + "days": 9.863889, + "weight": 5.2814368422107004e-05 + }, + { + "days": 9.931944, + "weight": 0.00010380755172621031 + }, + { + "days": 9.985417, + "weight": 4.5529627950092245e-05 + }, + { + "days": 10.048611, + "weight": 4.188725771408486e-05 + }, + { + "days": 10.845833, + "weight": 4.006607259608118e-05 + }, + { + "days": 10.914583, + "weight": 3.46025172420701e-05 + }, + { + "days": 11.076389, + "weight": 7.10262196021439e-05 + }, + { + "days": 11.150694, + "weight": 3.46025172420701e-05 + }, + { + "days": 11.209722, + "weight": 5.4635553540110695e-05 + }, + { + "days": 11.26875, + "weight": 4.917199818609962e-05 + }, + { + "days": 11.844444, + "weight": 4.188725771408486e-05 + }, + { + "days": 11.904861, + "weight": 4.006607259608118e-05 + }, + { + "days": 12.906944, + "weight": 4.370844283208855e-05 + }, + { + "days": 16.880556, + "weight": 4.5529627950092245e-05 + }, + { + "days": 17.958333, + "weight": 5.4635553540110695e-05 + }, + { + "new_client": true, + "weight": 0.022094617851620764 + } + ] +} \ No newline at end of file diff --git a/tools/DeltaIndexTestTool/dist-tail-10.json b/tools/DeltaIndexTestTool/dist-tail-10.json new file mode 100644 index 0000000000..6f560ff37c --- /dev/null +++ b/tools/DeltaIndexTestTool/dist-tail-10.json @@ -0,0 +1,7301 @@ +{ + "description": "Telemetry-derived from C:\\Users\\johnmcp\\Downloads\\export.csv: 610,103 download events, 1.99% net-new clients, observed ages 0-18.0 days, plus 10.0% reinstated stale tail out to 344 days", + "buckets": [ + { + "days": 0.0, + "weight": 4.5893864973692975e-05 + }, + { + "days": 0.000694, + "weight": 0.00048680278204238624 + }, + { + "days": 0.028472, + "weight": 0.0019603236610191716 + }, + { + "days": 0.042361, + "weight": 5.245013139850626e-05 + }, + { + "days": 0.043056, + "weight": 7.375799727914943e-05 + }, + { + "days": 0.047222, + "weight": 0.007354491862034299 + }, + { + "days": 0.049306, + "weight": 4.5893864973692975e-05 + }, + { + "days": 0.050694, + "weight": 0.0031929017488840686 + }, + { + "days": 0.051389, + "weight": 0.007974059039179155 + }, + { + "days": 0.052083, + "weight": 3.278133212406641e-05 + }, + { + "days": 0.054167, + "weight": 0.004607416230037535 + }, + { + "days": 0.054861, + "weight": 0.0072151712005070175 + }, + { + "days": 0.055556, + "weight": 0.005930142981243614 + }, + { + "days": 0.056944, + "weight": 0.006303850167457971 + }, + { + "days": 0.057639, + "weight": 0.008698526479121022 + }, + { + "days": 0.058333, + "weight": 0.00386000185760882 + }, + { + "days": 0.059028, + "weight": 0.039824401330922084 + }, + { + "days": 0.059722, + "weight": 0.018177248662794826 + }, + { + "days": 0.060417, + "weight": 0.007629855051876458 + }, + { + "days": 0.061111, + "weight": 0.01636280192972775 + }, + { + "days": 0.061806, + "weight": 0.018300178658260075 + }, + { + "days": 0.0625, + "weight": 0.003414175740721517 + }, + { + "days": 0.063194, + "weight": 0.012627369134190382 + }, + { + "days": 0.063889, + "weight": 0.014064830547830694 + }, + { + "days": 0.064583, + "weight": 0.0033289442771989443 + }, + { + "days": 0.065278, + "weight": 0.0074036638602204 + }, + { + "days": 0.065972, + "weight": 0.009255809125230151 + }, + { + "days": 0.066667, + "weight": 0.015251514770721898 + }, + { + "days": 0.067361, + "weight": 0.004218957444367347 + }, + { + "days": 0.069444, + "weight": 0.005913752315181581 + }, + { + "days": 0.070833, + "weight": 0.003612502800072119 + }, + { + "days": 0.072222, + "weight": 0.0038223033256661438 + }, + { + "days": 0.072917, + "weight": 0.003353530276291994 + }, + { + "days": 0.074306, + "weight": 0.011679988635804862 + }, + { + "days": 0.076389, + "weight": 5.9006397823319545e-05 + }, + { + "days": 0.078472, + "weight": 0.0047319852921089865 + }, + { + "days": 0.084028, + "weight": 0.0031404516174855625 + }, + { + "days": 0.084722, + "weight": 0.004556605165245232 + }, + { + "days": 0.085417, + "weight": 0.004640197562161601 + }, + { + "days": 0.0875, + "weight": 0.00021635679201883833 + }, + { + "days": 0.088889, + "weight": 0.002183236719462823 + }, + { + "days": 0.102083, + "weight": 0.0011735716900415777 + }, + { + "days": 0.109722, + "weight": 0.001124399691855478 + }, + { + "days": 0.110417, + "weight": 0.0018111685998546693 + }, + { + "days": 0.113194, + "weight": 0.004522184766514962 + }, + { + "days": 0.114583, + "weight": 0.0014522130130961422 + }, + { + "days": 0.115278, + "weight": 6.556266424813282e-05 + }, + { + "days": 0.115972, + "weight": 3.7698531942676374e-05 + }, + { + "days": 0.116667, + "weight": 0.001471881812370582 + }, + { + "days": 0.117361, + "weight": 0.0029355682917101473 + }, + { + "days": 0.118056, + "weight": 0.003019160688626517 + }, + { + "days": 0.11875, + "weight": 0.0019439329949571384 + }, + { + "days": 0.119444, + "weight": 0.0015308882101939016 + }, + { + "days": 0.120139, + "weight": 0.0019750752604750016 + }, + { + "days": 0.120833, + "weight": 0.00172265900311969 + }, + { + "days": 0.121528, + "weight": 0.00174724500221274 + }, + { + "days": 0.122222, + "weight": 0.001589894608017221 + }, + { + "days": 0.122917, + "weight": 0.001271915686413777 + }, + { + "days": 0.123611, + "weight": 0.0006097327775076353 + }, + { + "days": 0.124306, + "weight": 0.0018029732668236526 + }, + { + "days": 0.125, + "weight": 0.0006703782419371581 + }, + { + "days": 0.125694, + "weight": 0.004197649578486704 + }, + { + "days": 0.126389, + "weight": 0.0007736394381279673 + }, + { + "days": 0.127083, + "weight": 0.0025176063071283006 + }, + { + "days": 0.127778, + "weight": 0.00253727510640274 + }, + { + "days": 0.130556, + "weight": 0.00027864132305456454 + }, + { + "days": 0.13125, + "weight": 0.0013194486179936731 + }, + { + "days": 0.131944, + "weight": 0.0007047986406674278 + }, + { + "days": 0.132639, + "weight": 0.0013620643497549594 + }, + { + "days": 0.133333, + "weight": 0.002709377100054089 + }, + { + "days": 0.134028, + "weight": 0.0021045615223650635 + }, + { + "days": 0.134722, + "weight": 0.000649070376056515 + }, + { + "days": 0.136111, + "weight": 0.001339117417268113 + }, + { + "days": 0.1375, + "weight": 0.0005589217127153324 + }, + { + "days": 0.138194, + "weight": 0.0004933590484671995 + }, + { + "days": 0.14375, + "weight": 0.0003917369188825936 + }, + { + "days": 0.144444, + "weight": 0.0011555419573733411 + }, + { + "days": 0.147917, + "weight": 0.0010621151608197518 + }, + { + "days": 0.148611, + "weight": 0.0002393037245056848 + }, + { + "days": 0.149306, + "weight": 0.000899847566805623 + }, + { + "days": 0.150694, + "weight": 0.0012309390212586937 + }, + { + "days": 0.161111, + "weight": 0.0006949642410302079 + }, + { + "days": 0.164583, + "weight": 3.4420398730269734e-05 + }, + { + "days": 0.168056, + "weight": 0.000685129841392988 + }, + { + "days": 0.169444, + "weight": 0.001539083543224918 + }, + { + "days": 0.172222, + "weight": 0.000685129841392988 + }, + { + "days": 0.172917, + "weight": 6.064546442952286e-05 + }, + { + "days": 0.173611, + "weight": 0.0010178603624522622 + }, + { + "days": 0.175, + "weight": 0.0005179450475602494 + }, + { + "days": 0.175694, + "weight": 0.0012604422201703536 + }, + { + "days": 0.176389, + "weight": 0.0005851467784145854 + }, + { + "days": 0.177083, + "weight": 0.0010063868962088389 + }, + { + "days": 0.177778, + "weight": 0.001855423398222159 + }, + { + "days": 0.178472, + "weight": 0.0006048155776890253 + }, + { + "days": 0.179167, + "weight": 0.00219634925231245 + }, + { + "days": 0.179861, + "weight": 0.0010113040960274488 + }, + { + "days": 0.18125, + "weight": 0.0023930372450568483 + }, + { + "days": 0.181944, + "weight": 0.0013014188853254366 + }, + { + "days": 0.182639, + "weight": 0.0005261403805912659 + }, + { + "days": 0.183333, + "weight": 0.0005245013139850626 + }, + { + "days": 0.184028, + "weight": 0.0009949134299654155 + }, + { + "days": 0.184722, + "weight": 0.0013522299501177396 + }, + { + "days": 0.185417, + "weight": 0.0006916861078178013 + }, + { + "days": 0.186111, + "weight": 0.0024831859083980306 + }, + { + "days": 0.186806, + "weight": 0.0002983101223290044 + }, + { + "days": 0.1875, + "weight": 0.0004277963842190667 + }, + { + "days": 0.188889, + "weight": 0.0007867519709775939 + }, + { + "days": 0.190972, + "weight": 0.0004818855822237763 + }, + { + "days": 0.192361, + "weight": 0.001406319148122449 + }, + { + "days": 0.194444, + "weight": 0.0006359578432068885 + }, + { + "days": 0.195139, + "weight": 0.0027569100316339854 + }, + { + "days": 0.196528, + "weight": 0.0011375122247051046 + }, + { + "days": 0.197222, + "weight": 0.00039829318530740693 + }, + { + "days": 0.197917, + "weight": 0.0002589725237801247 + }, + { + "days": 0.2, + "weight": 0.00035567745354612057 + }, + { + "days": 0.202083, + "weight": 0.0014505739464899389 + }, + { + "days": 0.204167, + "weight": 0.000608093710901432 + }, + { + "days": 0.208333, + "weight": 0.0006523485092689216 + }, + { + "days": 0.210417, + "weight": 0.0007949473040086106 + }, + { + "days": 0.211111, + "weight": 0.00048680278204238624 + }, + { + "days": 0.2125, + "weight": 0.0005917030448393988 + }, + { + "days": 0.218056, + "weight": 0.00020324425916921175 + }, + { + "days": 0.222917, + "weight": 0.0003278133212406641 + }, + { + "days": 0.227083, + "weight": 0.00031470078839103757 + }, + { + "days": 0.229167, + "weight": 0.00010653932940321584 + }, + { + "days": 0.23125, + "weight": 0.0007211893067294611 + }, + { + "days": 0.232639, + "weight": 0.0001196518622528424 + }, + { + "days": 0.233333, + "weight": 9.834399637219924e-05 + }, + { + "days": 0.234722, + "weight": 0.00010817839600941917 + }, + { + "days": 0.235417, + "weight": 0.00010490026279701252 + }, + { + "days": 0.236111, + "weight": 0.0002770022564483612 + }, + { + "days": 0.2375, + "weight": 0.0002737241232359546 + }, + { + "days": 0.238194, + "weight": 0.00011309559582802913 + }, + { + "days": 0.238889, + "weight": 0.0004277963842190667 + }, + { + "days": 0.239583, + "weight": 0.00022783025826226156 + }, + { + "days": 0.240278, + "weight": 0.0004179619845818468 + }, + { + "days": 0.240972, + "weight": 0.0004441870502810999 + }, + { + "days": 0.242361, + "weight": 0.0003655118531833405 + }, + { + "days": 0.243056, + "weight": 0.0008113379700706437 + }, + { + "days": 0.24375, + "weight": 0.0005589217127153324 + }, + { + "days": 0.244444, + "weight": 0.00011473466243423244 + }, + { + "days": 0.245833, + "weight": 0.00024258185771809144 + }, + { + "days": 0.247917, + "weight": 0.00030650545536002095 + }, + { + "days": 0.248611, + "weight": 8.195333031016603e-05 + }, + { + "days": 0.249306, + "weight": 0.00025077719074910807 + }, + { + "days": 0.251389, + "weight": 0.00012784719528385901 + }, + { + "days": 0.252083, + "weight": 0.001186684222891204 + }, + { + "days": 0.252778, + "weight": 0.0002770022564483612 + }, + { + "days": 0.253472, + "weight": 0.0004687730493741497 + }, + { + "days": 0.254167, + "weight": 0.00023602559129327818 + }, + { + "days": 0.254861, + "weight": 0.0001114565292218258 + }, + { + "days": 0.255556, + "weight": 0.0001344034617086723 + }, + { + "days": 0.25625, + "weight": 0.00012129092885904572 + }, + { + "days": 0.258333, + "weight": 0.0002622506569925313 + }, + { + "days": 0.259028, + "weight": 8.850959673497932e-05 + }, + { + "days": 0.259722, + "weight": 7.867519709775939e-05 + }, + { + "days": 0.261111, + "weight": 0.0002540553239615147 + }, + { + "days": 0.265278, + "weight": 0.0001507941277707055 + }, + { + "days": 0.268056, + "weight": 0.00019996612595680512 + }, + { + "days": 0.271528, + "weight": 0.00044910425009970985 + }, + { + "days": 0.274306, + "weight": 0.00010653932940321584 + }, + { + "days": 0.275, + "weight": 0.00011473466243423244 + }, + { + "days": 0.276389, + "weight": 0.00012948626189006234 + }, + { + "days": 0.279861, + "weight": 0.00022783025826226156 + }, + { + "days": 0.281944, + "weight": 0.00016062852740792542 + }, + { + "days": 0.284028, + "weight": 4.2615731761286336e-05 + }, + { + "days": 0.286806, + "weight": 0.00013112532849626565 + }, + { + "days": 0.288194, + "weight": 8.850959673497932e-05 + }, + { + "days": 0.290278, + "weight": 0.0002393037245056848 + }, + { + "days": 0.292361, + "weight": 0.00010981746261562248 + }, + { + "days": 0.293056, + "weight": 0.0001425987947396889 + }, + { + "days": 0.295139, + "weight": 0.00013112532849626565 + }, + { + "days": 0.297222, + "weight": 7.867519709775939e-05 + }, + { + "days": 0.297917, + "weight": 0.00039501505209500027 + }, + { + "days": 0.299306, + "weight": 0.0004458261168873032 + }, + { + "days": 0.3, + "weight": 0.00024258185771809144 + }, + { + "days": 0.300694, + "weight": 0.0003343695876654774 + }, + { + "days": 0.301389, + "weight": 0.0002065223923816184 + }, + { + "days": 0.303472, + "weight": 7.375799727914943e-05 + }, + { + "days": 0.304167, + "weight": 0.00034584305390890065 + }, + { + "days": 0.305556, + "weight": 0.00035567745354612057 + }, + { + "days": 0.30625, + "weight": 0.00019832705935060179 + }, + { + "days": 0.306944, + "weight": 0.00015735039419551879 + }, + { + "days": 0.308333, + "weight": 0.0002720850566297512 + }, + { + "days": 0.309722, + "weight": 0.001145707557736121 + }, + { + "days": 0.310417, + "weight": 0.00012948626189006234 + }, + { + "days": 0.311111, + "weight": 0.0001819363932885686 + }, + { + "days": 0.311806, + "weight": 0.00043599171725008327 + }, + { + "days": 0.3125, + "weight": 0.0002753631898421579 + }, + { + "days": 0.313194, + "weight": 0.00040157131851981353 + }, + { + "days": 0.313889, + "weight": 0.00012456906207145238 + }, + { + "days": 0.314583, + "weight": 0.000168823860438942 + }, + { + "days": 0.315972, + "weight": 0.00012620812867765568 + }, + { + "days": 0.316667, + "weight": 9.834399637219924e-05 + }, + { + "days": 0.317361, + "weight": 0.00012129092885904572 + }, + { + "days": 0.31875, + "weight": 4.75329315798963e-05 + }, + { + "days": 0.320139, + "weight": 0.000337647720877884 + }, + { + "days": 0.320833, + "weight": 9.014866334118263e-05 + }, + { + "days": 0.322222, + "weight": 0.00018521452650097523 + }, + { + "days": 0.323611, + "weight": 4.917199818609962e-05 + }, + { + "days": 0.326389, + "weight": 0.00017046292704514534 + }, + { + "days": 0.327778, + "weight": 0.00013768159492107894 + }, + { + "days": 0.33125, + "weight": 0.00020160519256300844 + }, + { + "days": 0.332639, + "weight": 0.006790652949500357 + }, + { + "days": 0.335417, + "weight": 0.00018849265971338186 + }, + { + "days": 0.338194, + "weight": 9.50658631597926e-05 + }, + { + "days": 0.340278, + "weight": 0.00015571132758931546 + }, + { + "days": 0.340972, + "weight": 0.0003392867874840874 + }, + { + "days": 0.343056, + "weight": 0.00013112532849626565 + }, + { + "days": 0.34375, + "weight": 0.00016554572722653538 + }, + { + "days": 0.345139, + "weight": 3.1142265517863095e-05 + }, + { + "days": 0.345833, + "weight": 0.00015735039419551879 + }, + { + "days": 0.349306, + "weight": 0.00010653932940321584 + }, + { + "days": 0.353472, + "weight": 0.0001344034617086723 + }, + { + "days": 0.354861, + "weight": 0.00012292999546524905 + }, + { + "days": 0.356944, + "weight": 7.703613049155608e-05 + }, + { + "days": 0.357639, + "weight": 0.00015407226098311215 + }, + { + "days": 0.358333, + "weight": 6.720173085433615e-05 + }, + { + "days": 0.359028, + "weight": 0.000603176511082822 + }, + { + "days": 0.359722, + "weight": 0.0002474990575367014 + }, + { + "days": 0.360417, + "weight": 0.00019668799274439848 + }, + { + "days": 0.361111, + "weight": 0.00011473466243423244 + }, + { + "days": 0.361806, + "weight": 4.917199818609962e-05 + }, + { + "days": 0.3625, + "weight": 0.0003081445219662243 + }, + { + "days": 0.363194, + "weight": 0.00023110839147466822 + }, + { + "days": 0.363889, + "weight": 9.178772994738595e-05 + }, + { + "days": 0.365972, + "weight": 0.00019340985953199182 + }, + { + "days": 0.366667, + "weight": 9.342679655358928e-05 + }, + { + "days": 0.367361, + "weight": 6.884079746053947e-05 + }, + { + "days": 0.36875, + "weight": 0.00011473466243423244 + }, + { + "days": 0.369444, + "weight": 0.0001425987947396889 + }, + { + "days": 0.370139, + "weight": 0.0002720850566297512 + }, + { + "days": 0.371528, + "weight": 0.00042615731761286334 + }, + { + "days": 0.372222, + "weight": 0.00016226759401412875 + }, + { + "days": 0.372917, + "weight": 0.00022619119165605826 + }, + { + "days": 0.373611, + "weight": 0.00020160519256300844 + }, + { + "days": 0.375, + "weight": 0.0016489010058405407 + }, + { + "days": 0.376389, + "weight": 0.000388458785670187 + }, + { + "days": 0.377083, + "weight": 0.00015571132758931546 + }, + { + "days": 0.377778, + "weight": 0.0001835754598947719 + }, + { + "days": 0.378472, + "weight": 0.00016226759401412875 + }, + { + "days": 0.379861, + "weight": 0.00013112532849626565 + }, + { + "days": 0.38125, + "weight": 0.00013768159492107894 + }, + { + "days": 0.3875, + "weight": 0.00012948626189006234 + }, + { + "days": 0.388889, + "weight": 5.081106479230294e-05 + }, + { + "days": 0.390278, + "weight": 0.00011637372904043576 + }, + { + "days": 0.392361, + "weight": 0.00011637372904043576 + }, + { + "days": 0.397222, + "weight": 0.00021143959220022837 + }, + { + "days": 0.398611, + "weight": 0.0002393037245056848 + }, + { + "days": 0.399306, + "weight": 0.00013112532849626565 + }, + { + "days": 0.400694, + "weight": 0.000214717725412635 + }, + { + "days": 0.404167, + "weight": 0.00013276439510246898 + }, + { + "days": 0.406944, + "weight": 0.0018685359310717856 + }, + { + "days": 0.407639, + "weight": 0.00016390666062033205 + }, + { + "days": 0.409028, + "weight": 0.00012620812867765568 + }, + { + "days": 0.4125, + "weight": 0.00026552879020493795 + }, + { + "days": 0.414583, + "weight": 7.211893067294611e-05 + }, + { + "days": 0.415972, + "weight": 8.850959673497932e-05 + }, + { + "days": 0.418056, + "weight": 0.0002737241232359546 + }, + { + "days": 0.41875, + "weight": 0.00010817839600941917 + }, + { + "days": 0.419444, + "weight": 0.000214717725412635 + }, + { + "days": 0.420139, + "weight": 0.0003114226551786309 + }, + { + "days": 0.421528, + "weight": 0.00042615731761286334 + }, + { + "days": 0.422222, + "weight": 0.00021307865880643167 + }, + { + "days": 0.422917, + "weight": 8.687053012877599e-05 + }, + { + "days": 0.424306, + "weight": 0.00021963492523124496 + }, + { + "days": 0.425, + "weight": 0.0003097835885724276 + }, + { + "days": 0.426389, + "weight": 0.00012292999546524905 + }, + { + "days": 0.427083, + "weight": 0.000214717725412635 + }, + { + "days": 0.427778, + "weight": 0.0002737241232359546 + }, + { + "days": 0.429861, + "weight": 0.0015833383415924077 + }, + { + "days": 0.43125, + "weight": 4.917199818609962e-05 + }, + { + "days": 0.431944, + "weight": 0.00017865826007616194 + }, + { + "days": 0.432639, + "weight": 0.00021799585862504163 + }, + { + "days": 0.434028, + "weight": 0.00019177079292578852 + }, + { + "days": 0.435417, + "weight": 0.00013276439510246898 + }, + { + "days": 0.436806, + "weight": 4.425479836748966e-05 + }, + { + "days": 0.4375, + "weight": 0.0007441362392163075 + }, + { + "days": 0.438194, + "weight": 0.0002212739918374483 + }, + { + "days": 0.438889, + "weight": 0.0002720850566297512 + }, + { + "days": 0.440278, + "weight": 0.00014751599455829886 + }, + { + "days": 0.442361, + "weight": 0.000168823860438942 + }, + { + "days": 0.443056, + "weight": 0.00014587692795209553 + }, + { + "days": 0.446528, + "weight": 0.00011801279564663909 + }, + { + "days": 0.447222, + "weight": 0.0003360086542716807 + }, + { + "days": 0.447917, + "weight": 0.00020488332577541508 + }, + { + "days": 0.449306, + "weight": 0.00015407226098311215 + }, + { + "days": 0.453472, + "weight": 0.0003737071862143571 + }, + { + "days": 0.457639, + "weight": 0.00036059465336473053 + }, + { + "days": 0.458333, + "weight": 0.00043599171725008327 + }, + { + "days": 0.459028, + "weight": 7.867519709775939e-05 + }, + { + "days": 0.460417, + "weight": 0.00021963492523124496 + }, + { + "days": 0.4625, + "weight": 0.0009768836972971792 + }, + { + "days": 0.466667, + "weight": 0.0019849096601122214 + }, + { + "days": 0.468056, + "weight": 0.00042943545082527 + }, + { + "days": 0.470833, + "weight": 4.5893864973692975e-05 + }, + { + "days": 0.472222, + "weight": 0.00019340985953199182 + }, + { + "days": 0.472917, + "weight": 0.0001753801268637553 + }, + { + "days": 0.473611, + "weight": 0.0004818855822237763 + }, + { + "days": 0.474306, + "weight": 0.0005228622473788593 + }, + { + "days": 0.475, + "weight": 0.0002851975894793778 + }, + { + "days": 0.477083, + "weight": 0.0009473804983855193 + }, + { + "days": 0.477778, + "weight": 0.00031797892160344423 + }, + { + "days": 0.479167, + "weight": 0.00045074331670591315 + }, + { + "days": 0.480556, + "weight": 0.0004933590484671995 + }, + { + "days": 0.48125, + "weight": 0.0005425310466532991 + }, + { + "days": 0.483333, + "weight": 0.00042451825100666004 + }, + { + "days": 0.484028, + "weight": 0.0013161704847812665 + }, + { + "days": 0.484722, + "weight": 0.0002950319891165977 + }, + { + "days": 0.485417, + "weight": 0.0002851975894793778 + }, + { + "days": 0.486806, + "weight": 0.0011883232894974074 + }, + { + "days": 0.4875, + "weight": 0.0005343357136222825 + }, + { + "days": 0.488194, + "weight": 0.0003392867874840874 + }, + { + "days": 0.488889, + "weight": 0.0002983101223290044 + }, + { + "days": 0.490278, + "weight": 0.0007801957045527806 + }, + { + "days": 0.490972, + "weight": 0.00022783025826226156 + }, + { + "days": 0.491667, + "weight": 0.00039829318530740693 + }, + { + "days": 0.492361, + "weight": 0.0002540553239615147 + }, + { + "days": 0.493056, + "weight": 0.0007801957045527806 + }, + { + "days": 0.49375, + "weight": 0.0006310406433882784 + }, + { + "days": 0.494444, + "weight": 0.00346498680551382 + }, + { + "days": 0.495833, + "weight": 0.0003819025192453737 + }, + { + "days": 0.498611, + "weight": 0.0007375799727914943 + }, + { + "days": 0.499306, + "weight": 0.000260611590386328 + }, + { + "days": 0.500694, + "weight": 0.00029011478929798776 + }, + { + "days": 0.502083, + "weight": 0.00031797892160344423 + }, + { + "days": 0.50625, + "weight": 0.0003655118531833405 + }, + { + "days": 0.506944, + "weight": 0.0007490534390349175 + }, + { + "days": 0.509028, + "weight": 0.0003655118531833405 + }, + { + "days": 0.511111, + "weight": 0.0010916183597314115 + }, + { + "days": 0.5125, + "weight": 0.00013768159492107894 + }, + { + "days": 0.517361, + "weight": 0.0004458261168873032 + }, + { + "days": 0.51875, + "weight": 0.0002622506569925313 + }, + { + "days": 0.521528, + "weight": 0.0005408919800470958 + }, + { + "days": 0.522917, + "weight": 0.00026716785681114125 + }, + { + "days": 0.524306, + "weight": 0.00038681971906398365 + }, + { + "days": 0.525, + "weight": 0.00018029732668236527 + }, + { + "days": 0.526389, + "weight": 0.00025077719074910807 + }, + { + "days": 0.527083, + "weight": 0.0001819363932885686 + }, + { + "days": 0.532639, + "weight": 0.0004032103851260169 + }, + { + "days": 0.533333, + "weight": 0.0007244674399418677 + }, + { + "days": 0.535417, + "weight": 0.0004458261168873032 + }, + { + "days": 0.536111, + "weight": 0.00030486638875381765 + }, + { + "days": 0.536806, + "weight": 0.0013489518169053328 + }, + { + "days": 0.538194, + "weight": 8.523146352257267e-05 + }, + { + "days": 0.539583, + "weight": 0.000567117045746349 + }, + { + "days": 0.540278, + "weight": 0.0005408919800470958 + }, + { + "days": 0.543056, + "weight": 0.0004589386497369298 + }, + { + "days": 0.544444, + "weight": 0.0008490365020133201 + }, + { + "days": 0.545833, + "weight": 0.0003671509197895438 + }, + { + "days": 0.546528, + "weight": 0.0007064377072736312 + }, + { + "days": 0.547222, + "weight": 0.00031961798820964753 + }, + { + "days": 0.548611, + "weight": 0.00020160519256300844 + }, + { + "days": 0.549306, + "weight": 0.000301588255541411 + }, + { + "days": 0.55, + "weight": 0.001435822347034109 + }, + { + "days": 0.550694, + "weight": 0.0009539367648103326 + }, + { + "days": 0.551389, + "weight": 0.0004130447847632368 + }, + { + "days": 0.552083, + "weight": 9.670492976599591e-05 + }, + { + "days": 0.552778, + "weight": 0.00019504892613819515 + }, + { + "days": 0.553472, + "weight": 0.000506471581316826 + }, + { + "days": 0.554167, + "weight": 6.064546442952286e-05 + }, + { + "days": 0.554861, + "weight": 0.000260611590386328 + }, + { + "days": 0.555556, + "weight": 0.00023110839147466822 + }, + { + "days": 0.556944, + "weight": 0.00017374106025755198 + }, + { + "days": 0.557639, + "weight": 0.00029175385590419106 + }, + { + "days": 0.558333, + "weight": 0.0003228961214220542 + }, + { + "days": 0.559722, + "weight": 0.00039665411870120357 + }, + { + "days": 0.560417, + "weight": 0.0001032611961908092 + }, + { + "days": 0.568056, + "weight": 0.0001360425283148756 + }, + { + "days": 0.570139, + "weight": 0.0005277794471974693 + }, + { + "days": 0.570833, + "weight": 8.523146352257267e-05 + }, + { + "days": 0.572917, + "weight": 0.00014751599455829886 + }, + { + "days": 0.575, + "weight": 0.00016554572722653538 + }, + { + "days": 0.58125, + "weight": 0.00023274745808087152 + }, + { + "days": 0.584722, + "weight": 0.0006277625101758718 + }, + { + "days": 0.5875, + "weight": 3.605946533647306e-05 + }, + { + "days": 0.590278, + "weight": 7.211893067294611e-05 + }, + { + "days": 0.591667, + "weight": 0.0003638727865771372 + }, + { + "days": 0.592361, + "weight": 0.0002081614589878217 + }, + { + "days": 0.593056, + "weight": 8.359239691636936e-05 + }, + { + "days": 0.59375, + "weight": 0.00010817839600941917 + }, + { + "days": 0.595833, + "weight": 0.00011637372904043576 + }, + { + "days": 0.597222, + "weight": 0.0002458599909304981 + }, + { + "days": 0.597917, + "weight": 0.0010899792931252082 + }, + { + "days": 0.598611, + "weight": 0.00038518065245778034 + }, + { + "days": 0.599306, + "weight": 8.687053012877599e-05 + }, + { + "days": 0.6, + "weight": 0.00012456906207145238 + }, + { + "days": 0.601389, + "weight": 0.0002720850566297512 + }, + { + "days": 0.602083, + "weight": 5.57282646109129e-05 + }, + { + "days": 0.602778, + "weight": 0.00019668799274439848 + }, + { + "days": 0.603472, + "weight": 0.00020488332577541508 + }, + { + "days": 0.605556, + "weight": 0.00043599171725008327 + }, + { + "days": 0.606944, + "weight": 0.00016390666062033205 + }, + { + "days": 0.607639, + "weight": 0.0001425987947396889 + }, + { + "days": 0.609028, + "weight": 0.0001491550611645022 + }, + { + "days": 0.609722, + "weight": 0.00023438652468707485 + }, + { + "days": 0.610417, + "weight": 0.00012456906207145238 + }, + { + "days": 0.611111, + "weight": 0.0017046292704514534 + }, + { + "days": 0.611806, + "weight": 0.0002983101223290044 + }, + { + "days": 0.6125, + "weight": 6.392359764192951e-05 + }, + { + "days": 0.613194, + "weight": 4.917199818609962e-05 + }, + { + "days": 0.613889, + "weight": 0.0002835585228731745 + }, + { + "days": 0.614583, + "weight": 0.00025733345717392133 + }, + { + "days": 0.615278, + "weight": 7.211893067294611e-05 + }, + { + "days": 0.615972, + "weight": 7.211893067294611e-05 + }, + { + "days": 0.616667, + "weight": 0.0003360086542716807 + }, + { + "days": 0.617361, + "weight": 0.0002065223923816184 + }, + { + "days": 0.61875, + "weight": 0.00017865826007616194 + }, + { + "days": 0.619444, + "weight": 8.195333031016603e-05 + }, + { + "days": 0.620139, + "weight": 0.000296671055722801 + }, + { + "days": 0.622222, + "weight": 7.703613049155608e-05 + }, + { + "days": 0.623611, + "weight": 9.342679655358928e-05 + }, + { + "days": 0.624306, + "weight": 0.00010817839600941917 + }, + { + "days": 0.628472, + "weight": 7.703613049155608e-05 + }, + { + "days": 0.631944, + "weight": 0.0002458599909304981 + }, + { + "days": 0.632639, + "weight": 7.867519709775939e-05 + }, + { + "days": 0.634028, + "weight": 0.00040976665155083015 + }, + { + "days": 0.642361, + "weight": 0.0001032611961908092 + }, + { + "days": 0.644444, + "weight": 0.0004982762482858095 + }, + { + "days": 0.648611, + "weight": 0.00010653932940321584 + }, + { + "days": 0.652778, + "weight": 0.0015046631444946483 + }, + { + "days": 0.654167, + "weight": 0.0004212401177942534 + }, + { + "days": 0.654861, + "weight": 0.00012784719528385901 + }, + { + "days": 0.655556, + "weight": 0.0003638727865771372 + }, + { + "days": 0.65625, + "weight": 5.408919800470958e-05 + }, + { + "days": 0.656944, + "weight": 7.375799727914943e-05 + }, + { + "days": 0.657639, + "weight": 4.75329315798963e-05 + }, + { + "days": 0.658333, + "weight": 0.00019504892613819515 + }, + { + "days": 0.659028, + "weight": 6.720173085433615e-05 + }, + { + "days": 0.660417, + "weight": 0.00021635679201883833 + }, + { + "days": 0.661806, + "weight": 0.00016226759401412875 + }, + { + "days": 0.6625, + "weight": 6.884079746053947e-05 + }, + { + "days": 0.663194, + "weight": 9.998306297840256e-05 + }, + { + "days": 0.663889, + "weight": 5.408919800470958e-05 + }, + { + "days": 0.664583, + "weight": 0.0008031426370396271 + }, + { + "days": 0.665278, + "weight": 0.00011637372904043576 + }, + { + "days": 0.665972, + "weight": 0.00016226759401412875 + }, + { + "days": 0.666667, + "weight": 0.00010490026279701252 + }, + { + "days": 0.667361, + "weight": 0.0001425987947396889 + }, + { + "days": 0.668056, + "weight": 3.7698531942676374e-05 + }, + { + "days": 0.66875, + "weight": 0.0009424632985669094 + }, + { + "days": 0.669444, + "weight": 0.00019504892613819515 + }, + { + "days": 0.670139, + "weight": 0.0015243319437690881 + }, + { + "days": 0.670833, + "weight": 0.0001425987947396889 + }, + { + "days": 0.671528, + "weight": 0.00012784719528385901 + }, + { + "days": 0.672917, + "weight": 0.0002229130584436516 + }, + { + "days": 0.673611, + "weight": 0.00019340985953199182 + }, + { + "days": 0.674306, + "weight": 0.00015243319437690882 + }, + { + "days": 0.675, + "weight": 7.047986406674279e-05 + }, + { + "days": 0.675694, + "weight": 0.00022619119165605826 + }, + { + "days": 0.676389, + "weight": 0.0001753801268637553 + }, + { + "days": 0.677083, + "weight": 0.00020980052559402504 + }, + { + "days": 0.678472, + "weight": 9.014866334118263e-05 + }, + { + "days": 0.68125, + "weight": 5.408919800470958e-05 + }, + { + "days": 0.681944, + "weight": 0.0002540553239615147 + }, + { + "days": 0.682639, + "weight": 0.00011473466243423244 + }, + { + "days": 0.683333, + "weight": 0.0003523993203337139 + }, + { + "days": 0.684028, + "weight": 0.0004064885183384235 + }, + { + "days": 0.6875, + "weight": 0.00012129092885904572 + }, + { + "days": 0.688889, + "weight": 0.00016226759401412875 + }, + { + "days": 0.690278, + "weight": 5.081106479230294e-05 + }, + { + "days": 0.69375, + "weight": 9.342679655358928e-05 + }, + { + "days": 0.695833, + "weight": 6.228453103572619e-05 + }, + { + "days": 0.698611, + "weight": 0.0008146161032830504 + }, + { + "days": 0.70625, + "weight": 2.9503198911659772e-05 + }, + { + "days": 0.707639, + "weight": 0.0001032611961908092 + }, + { + "days": 0.713889, + "weight": 0.00039337598548879697 + }, + { + "days": 0.716667, + "weight": 0.00014587692795209553 + }, + { + "days": 0.71875, + "weight": 0.0007621659718845441 + }, + { + "days": 0.719444, + "weight": 0.0007015205074550212 + }, + { + "days": 0.720139, + "weight": 0.0013833722156356026 + }, + { + "days": 0.720833, + "weight": 0.0003523993203337139 + }, + { + "days": 0.721528, + "weight": 0.00018521452650097523 + }, + { + "days": 0.722222, + "weight": 8.850959673497932e-05 + }, + { + "days": 0.723611, + "weight": 0.00019340985953199182 + }, + { + "days": 0.725, + "weight": 8.195333031016603e-05 + }, + { + "days": 0.725694, + "weight": 0.00010981746261562248 + }, + { + "days": 0.726389, + "weight": 0.0003409258540902907 + }, + { + "days": 0.727083, + "weight": 0.0001032611961908092 + }, + { + "days": 0.727778, + "weight": 0.00014095972813348557 + }, + { + "days": 0.728472, + "weight": 0.00021635679201883833 + }, + { + "days": 0.729167, + "weight": 0.00028028038966076784 + }, + { + "days": 0.729861, + "weight": 0.00010162212958460587 + }, + { + "days": 0.730556, + "weight": 0.0012374952876835071 + }, + { + "days": 0.73125, + "weight": 9.014866334118263e-05 + }, + { + "days": 0.731944, + "weight": 6.884079746053947e-05 + }, + { + "days": 0.732639, + "weight": 0.00012620812867765568 + }, + { + "days": 0.733333, + "weight": 7.375799727914943e-05 + }, + { + "days": 0.734028, + "weight": 0.00023602559129327818 + }, + { + "days": 0.734722, + "weight": 8.359239691636936e-05 + }, + { + "days": 0.735417, + "weight": 0.00032125705481585084 + }, + { + "days": 0.736806, + "weight": 9.50658631597926e-05 + }, + { + "days": 0.7375, + "weight": 0.00013768159492107894 + }, + { + "days": 0.738194, + "weight": 0.000644153176237905 + }, + { + "days": 0.738889, + "weight": 0.00024913812414290476 + }, + { + "days": 0.740278, + "weight": 0.00013276439510246898 + }, + { + "days": 0.740972, + "weight": 5.736733121711622e-05 + }, + { + "days": 0.741667, + "weight": 0.00010162212958460587 + }, + { + "days": 0.743056, + "weight": 3.9337598548879697e-05 + }, + { + "days": 0.744444, + "weight": 9.670492976599591e-05 + }, + { + "days": 0.746528, + "weight": 9.342679655358928e-05 + }, + { + "days": 0.747222, + "weight": 0.000506471581316826 + }, + { + "days": 0.748611, + "weight": 0.00012292999546524905 + }, + { + "days": 0.749306, + "weight": 0.0001360425283148756 + }, + { + "days": 0.75, + "weight": 5.736733121711622e-05 + }, + { + "days": 0.750694, + "weight": 0.00010981746261562248 + }, + { + "days": 0.7625, + "weight": 6.556266424813282e-05 + }, + { + "days": 0.776389, + "weight": 0.000214717725412635 + }, + { + "days": 0.777083, + "weight": 5.9006397823319545e-05 + }, + { + "days": 0.777778, + "weight": 5.408919800470958e-05 + }, + { + "days": 0.778472, + "weight": 0.0004769683824051663 + }, + { + "days": 0.779167, + "weight": 0.0001819363932885686 + }, + { + "days": 0.779861, + "weight": 0.002270107249591599 + }, + { + "days": 0.780556, + "weight": 0.00042287918440045674 + }, + { + "days": 0.78125, + "weight": 0.0001032611961908092 + }, + { + "days": 0.781944, + "weight": 9.178772994738595e-05 + }, + { + "days": 0.782639, + "weight": 0.00031306172178483427 + }, + { + "days": 0.783333, + "weight": 8.850959673497932e-05 + }, + { + "days": 0.784028, + "weight": 0.00019340985953199182 + }, + { + "days": 0.785417, + "weight": 0.00042615731761286334 + }, + { + "days": 0.786806, + "weight": 0.0002851975894793778 + }, + { + "days": 0.7875, + "weight": 0.0026093940370756864 + }, + { + "days": 0.788194, + "weight": 0.0005556435795029257 + }, + { + "days": 0.788889, + "weight": 4.2615731761286336e-05 + }, + { + "days": 0.789583, + "weight": 0.000735940906185291 + }, + { + "days": 0.790972, + "weight": 6.884079746053947e-05 + }, + { + "days": 0.791667, + "weight": 0.0003278133212406641 + }, + { + "days": 0.792361, + "weight": 0.00028847572269178446 + }, + { + "days": 0.793056, + "weight": 9.178772994738595e-05 + }, + { + "days": 0.79375, + "weight": 0.00012784719528385901 + }, + { + "days": 0.795833, + "weight": 0.00041140571815703346 + }, + { + "days": 0.796528, + "weight": 0.00014587692795209553 + }, + { + "days": 0.797917, + "weight": 0.00017210199365134867 + }, + { + "days": 0.798611, + "weight": 0.00035567745354612057 + }, + { + "days": 0.799306, + "weight": 9.834399637219924e-05 + }, + { + "days": 0.8, + "weight": 0.0001835754598947719 + }, + { + "days": 0.800694, + "weight": 6.228453103572619e-05 + }, + { + "days": 0.801389, + "weight": 0.00037042905300195046 + }, + { + "days": 0.802778, + "weight": 8.687053012877599e-05 + }, + { + "days": 0.804167, + "weight": 0.00010981746261562248 + }, + { + "days": 0.804861, + "weight": 0.0003360086542716807 + }, + { + "days": 0.805556, + "weight": 8.195333031016603e-05 + }, + { + "days": 0.80625, + "weight": 5.9006397823319545e-05 + }, + { + "days": 0.808333, + "weight": 9.178772994738595e-05 + }, + { + "days": 0.809028, + "weight": 9.342679655358928e-05 + }, + { + "days": 0.810417, + "weight": 0.00035567745354612057 + }, + { + "days": 0.811111, + "weight": 0.0002720850566297512 + }, + { + "days": 0.827083, + "weight": 9.998306297840256e-05 + }, + { + "days": 0.832639, + "weight": 0.00017046292704514534 + }, + { + "days": 0.836806, + "weight": 7.539706388535275e-05 + }, + { + "days": 0.838194, + "weight": 0.0003409258540902907 + }, + { + "days": 0.838889, + "weight": 0.0008785397009249799 + }, + { + "days": 0.839583, + "weight": 0.0004786074490113696 + }, + { + "days": 0.840278, + "weight": 9.178772994738595e-05 + }, + { + "days": 0.840972, + "weight": 5.245013139850626e-05 + }, + { + "days": 0.841667, + "weight": 0.00022455212504985493 + }, + { + "days": 0.842361, + "weight": 0.00010490026279701252 + }, + { + "days": 0.84375, + "weight": 0.000337647720877884 + }, + { + "days": 0.844444, + "weight": 0.0001491550611645022 + }, + { + "days": 0.845833, + "weight": 0.0002589725237801247 + }, + { + "days": 0.846528, + "weight": 0.0007031595740612245 + }, + { + "days": 0.847222, + "weight": 0.0009342679655358928 + }, + { + "days": 0.849306, + "weight": 0.0028077210964262884 + }, + { + "days": 0.85, + "weight": 0.00017210199365134867 + }, + { + "days": 0.850694, + "weight": 0.00048680278204238624 + }, + { + "days": 0.851389, + "weight": 0.00022783025826226156 + }, + { + "days": 0.852778, + "weight": 7.539706388535275e-05 + }, + { + "days": 0.854167, + "weight": 8.687053012877599e-05 + }, + { + "days": 0.854861, + "weight": 0.0005474482464719091 + }, + { + "days": 0.855556, + "weight": 0.00023766465789948148 + }, + { + "days": 0.856944, + "weight": 0.00028847572269178446 + }, + { + "days": 0.857639, + "weight": 0.00010817839600941917 + }, + { + "days": 0.858333, + "weight": 9.834399637219924e-05 + }, + { + "days": 0.860417, + "weight": 0.00023602559129327818 + }, + { + "days": 0.861111, + "weight": 0.0004671339827679464 + }, + { + "days": 0.861806, + "weight": 0.00013932066152728227 + }, + { + "days": 0.863889, + "weight": 0.00038681971906398365 + }, + { + "days": 0.864583, + "weight": 0.00018849265971338186 + }, + { + "days": 0.865278, + "weight": 0.00030486638875381765 + }, + { + "days": 0.865972, + "weight": 0.00017701919346995863 + }, + { + "days": 0.866667, + "weight": 0.00012784719528385901 + }, + { + "days": 0.868056, + "weight": 0.00016554572722653538 + }, + { + "days": 0.86875, + "weight": 0.00030322732214761435 + }, + { + "days": 0.870139, + "weight": 0.0002720850566297512 + }, + { + "days": 0.870833, + "weight": 0.0002442209243242948 + }, + { + "days": 0.872222, + "weight": 0.00023438652468707485 + }, + { + "days": 0.882639, + "weight": 0.00010981746261562248 + }, + { + "days": 0.890278, + "weight": 5.736733121711622e-05 + }, + { + "days": 0.891667, + "weight": 0.0003261742546344608 + }, + { + "days": 0.896528, + "weight": 0.00047532931579896296 + }, + { + "days": 0.897917, + "weight": 0.00010817839600941917 + }, + { + "days": 0.898611, + "weight": 0.00042943545082527 + }, + { + "days": 0.901389, + "weight": 0.00024913812414290476 + }, + { + "days": 0.902083, + "weight": 0.0001114565292218258 + }, + { + "days": 0.902778, + "weight": 0.0002294693248684649 + }, + { + "days": 0.903472, + "weight": 0.00021799585862504163 + }, + { + "days": 0.904861, + "weight": 0.000337647720877884 + }, + { + "days": 0.905556, + "weight": 0.0036420059989837783 + }, + { + "days": 0.90625, + "weight": 0.0005736733121711622 + }, + { + "days": 0.906944, + "weight": 0.0005917030448393988 + }, + { + "days": 0.907639, + "weight": 0.00041140571815703346 + }, + { + "days": 0.908333, + "weight": 0.00046385584955553974 + }, + { + "days": 0.909722, + "weight": 7.539706388535275e-05 + }, + { + "days": 0.913194, + "weight": 0.00018685359310717856 + }, + { + "days": 0.913889, + "weight": 0.00022455212504985493 + }, + { + "days": 0.914583, + "weight": 0.00039993225191361023 + }, + { + "days": 0.915278, + "weight": 0.00011309559582802913 + }, + { + "days": 0.915972, + "weight": 0.0008293677027388802 + }, + { + "days": 0.916667, + "weight": 0.00040157131851981353 + }, + { + "days": 0.917361, + "weight": 3.4420398730269734e-05 + }, + { + "days": 0.918056, + "weight": 0.00020488332577541508 + }, + { + "days": 0.921528, + "weight": 0.010753916003299988 + }, + { + "days": 0.922222, + "weight": 0.000214717725412635 + }, + { + "days": 0.922917, + "weight": 0.0008670662346815566 + }, + { + "days": 0.923611, + "weight": 0.0002999491889352077 + }, + { + "days": 0.924306, + "weight": 0.002501215641066267 + }, + { + "days": 0.925, + "weight": 0.00019668799274439848 + }, + { + "days": 0.925694, + "weight": 0.00046385584955553974 + }, + { + "days": 0.926389, + "weight": 9.998306297840256e-05 + }, + { + "days": 0.927083, + "weight": 0.00019177079292578852 + }, + { + "days": 0.930556, + "weight": 0.0005408919800470958 + }, + { + "days": 0.93125, + "weight": 0.00031961798820964753 + }, + { + "days": 0.931944, + "weight": 0.00017701919346995863 + }, + { + "days": 0.932639, + "weight": 0.00027864132305456454 + }, + { + "days": 0.933333, + "weight": 0.00031961798820964753 + }, + { + "days": 0.94375, + "weight": 0.00042615731761286334 + }, + { + "days": 0.948611, + "weight": 0.00021799585862504163 + }, + { + "days": 0.950694, + "weight": 0.0002950319891165977 + }, + { + "days": 0.951389, + "weight": 0.0011702935568291709 + }, + { + "days": 0.955556, + "weight": 0.0007457753058225109 + }, + { + "days": 0.95625, + "weight": 0.002658566035261786 + }, + { + "days": 0.956944, + "weight": 0.0009145991662614529 + }, + { + "days": 0.957639, + "weight": 0.00031306172178483427 + }, + { + "days": 0.958333, + "weight": 0.00021307865880643167 + }, + { + "days": 0.959028, + "weight": 0.0005408919800470958 + }, + { + "days": 0.959722, + "weight": 6.228453103572619e-05 + }, + { + "days": 0.960417, + "weight": 0.003273216012588031 + }, + { + "days": 0.961111, + "weight": 0.0016374275395971173 + }, + { + "days": 0.961806, + "weight": 0.000214717725412635 + }, + { + "days": 0.9625, + "weight": 0.001257164086957947 + }, + { + "days": 0.964583, + "weight": 0.0010211384956646688 + }, + { + "days": 0.965278, + "weight": 0.00047205118258655636 + }, + { + "days": 0.965972, + "weight": 0.0014276270140030922 + }, + { + "days": 0.968056, + "weight": 0.0008211723697078636 + }, + { + "days": 0.96875, + "weight": 0.00644644896219766 + }, + { + "days": 0.970139, + "weight": 0.00044746518349350655 + }, + { + "days": 0.972222, + "weight": 0.0016243150067474907 + }, + { + "days": 0.972917, + "weight": 0.007910135441537226 + }, + { + "days": 0.973611, + "weight": 0.0014030410149100424 + }, + { + "days": 0.974306, + "weight": 0.001457130212914752 + }, + { + "days": 0.975, + "weight": 0.0006998814408488179 + }, + { + "days": 0.975694, + "weight": 0.0002294693248684649 + }, + { + "days": 0.976389, + "weight": 0.0008621490348629466 + }, + { + "days": 0.977083, + "weight": 0.003612502800072119 + }, + { + "days": 0.977778, + "weight": 4.2615731761286336e-05 + }, + { + "days": 0.979167, + "weight": 0.0015472788762559346 + }, + { + "days": 0.979861, + "weight": 0.0007900301041900005 + }, + { + "days": 0.980556, + "weight": 0.0011768498232539843 + }, + { + "days": 0.98125, + "weight": 0.0015751430085613912 + }, + { + "days": 0.981944, + "weight": 0.003568248001704629 + }, + { + "days": 0.982639, + "weight": 0.0046992039599849204 + }, + { + "days": 0.983333, + "weight": 0.0010948964929438183 + }, + { + "days": 0.984722, + "weight": 0.0003753462528205604 + }, + { + "days": 0.985417, + "weight": 0.0008605099682567433 + }, + { + "days": 0.986111, + "weight": 0.007726559981642453 + }, + { + "days": 0.986806, + "weight": 0.000296671055722801 + }, + { + "days": 0.988194, + "weight": 0.009711469641754674 + }, + { + "days": 0.988889, + "weight": 0.0010194994290584655 + }, + { + "days": 0.989583, + "weight": 0.00038518065245778034 + }, + { + "days": 0.990278, + "weight": 0.0016898776709956236 + }, + { + "days": 0.990972, + "weight": 0.003122421884817326 + }, + { + "days": 0.991667, + "weight": 3.7698531942676374e-05 + }, + { + "days": 0.995139, + "weight": 0.0010358900951204987 + }, + { + "days": 0.995833, + "weight": 0.00013276439510246898 + }, + { + "days": 0.998611, + "weight": 0.0005113887811354361 + }, + { + "days": 1.002778, + "weight": 0.00038518065245778034 + }, + { + "days": 1.007639, + "weight": 0.0013899284820604158 + }, + { + "days": 1.008333, + "weight": 4.2615731761286336e-05 + }, + { + "days": 1.009722, + "weight": 0.003143729750697969 + }, + { + "days": 1.011111, + "weight": 0.00038354158585157704 + }, + { + "days": 1.014583, + "weight": 8.195333031016603e-05 + }, + { + "days": 1.015278, + "weight": 0.002243882183892346 + }, + { + "days": 1.016667, + "weight": 0.002204544585343466 + }, + { + "days": 1.017361, + "weight": 0.001691516737601827 + }, + { + "days": 1.018056, + "weight": 0.020457190312023647 + }, + { + "days": 1.01875, + "weight": 0.0018931219301648353 + }, + { + "days": 1.020139, + "weight": 0.0005654779791401456 + }, + { + "days": 1.021528, + "weight": 0.00017210199365134867 + }, + { + "days": 1.022917, + "weight": 0.0014227098141844823 + }, + { + "days": 1.023611, + "weight": 0.00019832705935060179 + }, + { + "days": 1.024306, + "weight": 0.0003819025192453737 + }, + { + "days": 1.025, + "weight": 0.00025241625735531137 + }, + { + "days": 1.027083, + "weight": 0.0007474143724287142 + }, + { + "days": 1.027778, + "weight": 0.004643475695374008 + }, + { + "days": 1.028472, + "weight": 9.834399637219924e-05 + }, + { + "days": 1.029861, + "weight": 0.0019455720615633417 + }, + { + "days": 1.03125, + "weight": 0.00021635679201883833 + }, + { + "days": 1.031944, + "weight": 0.0022012664521310595 + }, + { + "days": 1.032639, + "weight": 0.0020013003261742544 + }, + { + "days": 1.033333, + "weight": 0.00024094279111188814 + }, + { + "days": 1.034028, + "weight": 0.0012047139555594407 + }, + { + "days": 1.034722, + "weight": 0.005361386868891062 + }, + { + "days": 1.035417, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.036111, + "weight": 0.0012735547530199802 + }, + { + "days": 1.036806, + "weight": 0.000260611590386328 + }, + { + "days": 1.038889, + "weight": 0.0008293677027388802 + }, + { + "days": 1.039583, + "weight": 0.0010588370276073452 + }, + { + "days": 1.040972, + "weight": 0.0005441701132595024 + }, + { + "days": 1.041667, + "weight": 0.0005851467784145854 + }, + { + "days": 1.042361, + "weight": 0.0013227267512060797 + }, + { + "days": 1.043056, + "weight": 0.00210783965557747 + }, + { + "days": 1.04375, + "weight": 0.0012391343542897104 + }, + { + "days": 1.044444, + "weight": 0.0004687730493741497 + }, + { + "days": 1.045139, + "weight": 0.0015259710103752914 + }, + { + "days": 1.045833, + "weight": 0.0006179281105386519 + }, + { + "days": 1.046528, + "weight": 0.0007605269052783408 + }, + { + "days": 1.048611, + "weight": 0.0005966202446580088 + }, + { + "days": 1.05, + "weight": 0.0006457922428441084 + }, + { + "days": 1.050694, + "weight": 0.0004769683824051663 + }, + { + "days": 1.051389, + "weight": 0.02323868634275068 + }, + { + "days": 1.052083, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.052778, + "weight": 5.081106479230294e-05 + }, + { + "days": 1.054861, + "weight": 0.00303555135468855 + }, + { + "days": 1.056944, + "weight": 0.00034584305390890065 + }, + { + "days": 1.058333, + "weight": 0.0005245013139850626 + }, + { + "days": 1.064583, + "weight": 0.000260611590386328 + }, + { + "days": 1.065972, + "weight": 7.211893067294611e-05 + }, + { + "days": 1.069444, + "weight": 0.00046549491616174304 + }, + { + "days": 1.073611, + "weight": 0.00039337598548879697 + }, + { + "days": 1.074306, + "weight": 0.00012948626189006234 + }, + { + "days": 1.076389, + "weight": 0.0003507602537275106 + }, + { + "days": 1.077083, + "weight": 0.0003097835885724276 + }, + { + "days": 1.077778, + "weight": 0.00019668799274439848 + }, + { + "days": 1.079167, + "weight": 9.014866334118263e-05 + }, + { + "days": 1.079861, + "weight": 0.0001671847938327387 + }, + { + "days": 1.08125, + "weight": 0.00010981746261562248 + }, + { + "days": 1.082639, + "weight": 0.0006179281105386519 + }, + { + "days": 1.084028, + "weight": 7.211893067294611e-05 + }, + { + "days": 1.084722, + "weight": 0.0015718648753489846 + }, + { + "days": 1.086111, + "weight": 0.00022783025826226156 + }, + { + "days": 1.0875, + "weight": 7.703613049155608e-05 + }, + { + "days": 1.089583, + "weight": 0.00036223371997093384 + }, + { + "days": 1.090972, + "weight": 0.00020980052559402504 + }, + { + "days": 1.091667, + "weight": 0.0008703443678939632 + }, + { + "days": 1.092361, + "weight": 0.0013178095513874698 + }, + { + "days": 1.09375, + "weight": 0.0005507263796843158 + }, + { + "days": 1.094444, + "weight": 0.00019177079292578852 + }, + { + "days": 1.095139, + "weight": 0.00027864132305456454 + }, + { + "days": 1.097917, + "weight": 0.0015407226098311214 + }, + { + "days": 1.098611, + "weight": 0.00010490026279701252 + }, + { + "days": 1.1, + "weight": 0.00017210199365134867 + }, + { + "days": 1.100694, + "weight": 0.0004917199818609962 + }, + { + "days": 1.101389, + "weight": 0.000470412115980353 + }, + { + "days": 1.102083, + "weight": 0.00040157131851981353 + }, + { + "days": 1.104167, + "weight": 0.00023602559129327818 + }, + { + "days": 1.104861, + "weight": 0.000168823860438942 + }, + { + "days": 1.105556, + "weight": 8.359239691636936e-05 + }, + { + "days": 1.10625, + "weight": 0.0002393037245056848 + }, + { + "days": 1.108333, + "weight": 0.00024258185771809144 + }, + { + "days": 1.109028, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.109722, + "weight": 8.850959673497932e-05 + }, + { + "days": 1.110417, + "weight": 0.000260611590386328 + }, + { + "days": 1.111806, + "weight": 0.00021307865880643167 + }, + { + "days": 1.1125, + "weight": 0.00025733345717392133 + }, + { + "days": 1.113889, + "weight": 0.00015571132758931546 + }, + { + "days": 1.115972, + "weight": 8.359239691636936e-05 + }, + { + "days": 1.116667, + "weight": 0.0001491550611645022 + }, + { + "days": 1.118056, + "weight": 0.0005179450475602494 + }, + { + "days": 1.120139, + "weight": 0.0004802465156175729 + }, + { + "days": 1.120833, + "weight": 0.010386765083510443 + }, + { + "days": 1.131944, + "weight": 7.539706388535275e-05 + }, + { + "days": 1.136111, + "weight": 0.00018685359310717856 + }, + { + "days": 1.136806, + "weight": 0.0002294693248684649 + }, + { + "days": 1.1375, + "weight": 0.0001819363932885686 + }, + { + "days": 1.138889, + "weight": 0.00044910425009970985 + }, + { + "days": 1.140278, + "weight": 5.081106479230294e-05 + }, + { + "days": 1.141667, + "weight": 9.834399637219924e-05 + }, + { + "days": 1.145833, + "weight": 0.0001344034617086723 + }, + { + "days": 1.146528, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.147222, + "weight": 0.00010817839600941917 + }, + { + "days": 1.147917, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.148611, + "weight": 8.195333031016603e-05 + }, + { + "days": 1.149306, + "weight": 0.0001114565292218258 + }, + { + "days": 1.15, + "weight": 0.00012948626189006234 + }, + { + "days": 1.150694, + "weight": 0.00032945238784686746 + }, + { + "days": 1.151389, + "weight": 0.0005753123787773655 + }, + { + "days": 1.152083, + "weight": 0.00011801279564663909 + }, + { + "days": 1.153472, + "weight": 0.00014423786134589223 + }, + { + "days": 1.154861, + "weight": 0.00012784719528385901 + }, + { + "days": 1.155556, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.156944, + "weight": 4.75329315798963e-05 + }, + { + "days": 1.157639, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.159028, + "weight": 0.0004376307838562866 + }, + { + "days": 1.159722, + "weight": 0.0004392698504624899 + }, + { + "days": 1.160417, + "weight": 0.0013718987493921795 + }, + { + "days": 1.161111, + "weight": 0.0002622506569925313 + }, + { + "days": 1.161806, + "weight": 6.720173085433615e-05 + }, + { + "days": 1.163194, + "weight": 0.00014095972813348557 + }, + { + "days": 1.163889, + "weight": 0.00033273052105927406 + }, + { + "days": 1.165278, + "weight": 0.0002999491889352077 + }, + { + "days": 1.165972, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.166667, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.167361, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.168056, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.170139, + "weight": 7.703613049155608e-05 + }, + { + "days": 1.170833, + "weight": 0.00011801279564663909 + }, + { + "days": 1.171528, + "weight": 0.0001491550611645022 + }, + { + "days": 1.172917, + "weight": 6.064546442952286e-05 + }, + { + "days": 1.174306, + "weight": 0.00026388972359873465 + }, + { + "days": 1.176389, + "weight": 0.00021307865880643167 + }, + { + "days": 1.177083, + "weight": 0.0001819363932885686 + }, + { + "days": 1.178472, + "weight": 0.0001032611961908092 + }, + { + "days": 1.181944, + "weight": 6.556266424813282e-05 + }, + { + "days": 1.184722, + "weight": 5.245013139850626e-05 + }, + { + "days": 1.1875, + "weight": 0.0001753801268637553 + }, + { + "days": 1.189583, + "weight": 2.786413230545645e-05 + }, + { + "days": 1.191667, + "weight": 9.014866334118263e-05 + }, + { + "days": 1.193056, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.19375, + "weight": 0.00016226759401412875 + }, + { + "days": 1.194444, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.198611, + "weight": 9.014866334118263e-05 + }, + { + "days": 1.199306, + "weight": 0.0017882216673678229 + }, + { + "days": 1.200694, + "weight": 4.097666515508301e-05 + }, + { + "days": 1.202778, + "weight": 9.998306297840256e-05 + }, + { + "days": 1.203472, + "weight": 6.064546442952286e-05 + }, + { + "days": 1.204861, + "weight": 9.342679655358928e-05 + }, + { + "days": 1.205556, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.206944, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.209028, + "weight": 0.0001753801268637553 + }, + { + "days": 1.209722, + "weight": 8.850959673497932e-05 + }, + { + "days": 1.210417, + "weight": 0.00046549491616174304 + }, + { + "days": 1.211111, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.2125, + "weight": 0.0002540553239615147 + }, + { + "days": 1.213194, + "weight": 0.00010490026279701252 + }, + { + "days": 1.213889, + "weight": 0.00019177079292578852 + }, + { + "days": 1.214583, + "weight": 0.0001360425283148756 + }, + { + "days": 1.215278, + "weight": 0.00012129092885904572 + }, + { + "days": 1.215972, + "weight": 4.75329315798963e-05 + }, + { + "days": 1.218056, + "weight": 0.00013932066152728227 + }, + { + "days": 1.219444, + "weight": 0.00012948626189006234 + }, + { + "days": 1.220139, + "weight": 4.2615731761286336e-05 + }, + { + "days": 1.220833, + "weight": 0.0007933082374024071 + }, + { + "days": 1.222222, + "weight": 0.0003392867874840874 + }, + { + "days": 1.222917, + "weight": 6.720173085433615e-05 + }, + { + "days": 1.224306, + "weight": 0.00012948626189006234 + }, + { + "days": 1.225694, + "weight": 0.0009752446306909758 + }, + { + "days": 1.226389, + "weight": 0.0001114565292218258 + }, + { + "days": 1.228472, + "weight": 0.0001360425283148756 + }, + { + "days": 1.229167, + "weight": 5.081106479230294e-05 + }, + { + "days": 1.229861, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.231944, + "weight": 0.00015243319437690882 + }, + { + "days": 1.232639, + "weight": 9.50658631597926e-05 + }, + { + "days": 1.233333, + "weight": 0.00012292999546524905 + }, + { + "days": 1.234028, + "weight": 0.00015735039419551879 + }, + { + "days": 1.234722, + "weight": 8.031426370396271e-05 + }, + { + "days": 1.2375, + "weight": 0.00042615731761286334 + }, + { + "days": 1.238194, + "weight": 8.359239691636936e-05 + }, + { + "days": 1.238889, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.240278, + "weight": 0.00016554572722653538 + }, + { + "days": 1.246528, + "weight": 0.0017554403352437564 + }, + { + "days": 1.251389, + "weight": 7.047986406674279e-05 + }, + { + "days": 1.254167, + "weight": 0.00017374106025755198 + }, + { + "days": 1.25625, + "weight": 0.00011473466243423244 + }, + { + "days": 1.258333, + "weight": 0.0001196518622528424 + }, + { + "days": 1.261806, + "weight": 0.00025733345717392133 + }, + { + "days": 1.263889, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.264583, + "weight": 3.278133212406641e-05 + }, + { + "days": 1.265972, + "weight": 0.00010981746261562248 + }, + { + "days": 1.267361, + "weight": 0.0002442209243242948 + }, + { + "days": 1.268056, + "weight": 0.00019340985953199182 + }, + { + "days": 1.26875, + "weight": 0.00012620812867765568 + }, + { + "days": 1.270139, + "weight": 9.014866334118263e-05 + }, + { + "days": 1.272222, + "weight": 0.0001032611961908092 + }, + { + "days": 1.273611, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.274306, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.275, + "weight": 0.00013932066152728227 + }, + { + "days": 1.276389, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.277083, + "weight": 0.00015407226098311215 + }, + { + "days": 1.278472, + "weight": 9.50658631597926e-05 + }, + { + "days": 1.279167, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.279861, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.281944, + "weight": 4.097666515508301e-05 + }, + { + "days": 1.282639, + "weight": 0.0003163398549972409 + }, + { + "days": 1.283333, + "weight": 0.00010981746261562248 + }, + { + "days": 1.284028, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.284722, + "weight": 0.0003819025192453737 + }, + { + "days": 1.285417, + "weight": 0.0001425987947396889 + }, + { + "days": 1.286111, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.288194, + "weight": 0.00032945238784686746 + }, + { + "days": 1.290278, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.29375, + "weight": 0.00013768159492107894 + }, + { + "days": 1.294444, + "weight": 0.00011801279564663909 + }, + { + "days": 1.295139, + "weight": 4.2615731761286336e-05 + }, + { + "days": 1.295833, + "weight": 5.245013139850626e-05 + }, + { + "days": 1.296528, + "weight": 0.00017046292704514534 + }, + { + "days": 1.297917, + "weight": 0.00011801279564663909 + }, + { + "days": 1.298611, + "weight": 0.0005294185138036726 + }, + { + "days": 1.299306, + "weight": 0.00015243319437690882 + }, + { + "days": 1.303472, + "weight": 0.00025569439056771803 + }, + { + "days": 1.306944, + "weight": 0.00010653932940321584 + }, + { + "days": 1.313194, + "weight": 9.670492976599591e-05 + }, + { + "days": 1.315972, + "weight": 6.720173085433615e-05 + }, + { + "days": 1.316667, + "weight": 5.081106479230294e-05 + }, + { + "days": 1.317361, + "weight": 4.75329315798963e-05 + }, + { + "days": 1.322917, + "weight": 9.670492976599591e-05 + }, + { + "days": 1.325, + "weight": 7.211893067294611e-05 + }, + { + "days": 1.325694, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.326389, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.327083, + "weight": 9.342679655358928e-05 + }, + { + "days": 1.327778, + "weight": 3.1142265517863095e-05 + }, + { + "days": 1.328472, + "weight": 0.00016554572722653538 + }, + { + "days": 1.329167, + "weight": 5.081106479230294e-05 + }, + { + "days": 1.329861, + "weight": 9.342679655358928e-05 + }, + { + "days": 1.330556, + "weight": 0.00010162212958460587 + }, + { + "days": 1.33125, + "weight": 0.0006539875758751249 + }, + { + "days": 1.332639, + "weight": 0.0001114565292218258 + }, + { + "days": 1.333333, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.334722, + "weight": 0.00012292999546524905 + }, + { + "days": 1.336806, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.338194, + "weight": 0.0001114565292218258 + }, + { + "days": 1.338889, + "weight": 9.014866334118263e-05 + }, + { + "days": 1.339583, + "weight": 8.523146352257267e-05 + }, + { + "days": 1.340278, + "weight": 5.081106479230294e-05 + }, + { + "days": 1.340972, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.342361, + "weight": 0.0003638727865771372 + }, + { + "days": 1.343056, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.344444, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.345139, + "weight": 0.00011801279564663909 + }, + { + "days": 1.346528, + "weight": 0.00019668799274439848 + }, + { + "days": 1.348611, + "weight": 0.000214717725412635 + }, + { + "days": 1.35, + "weight": 0.00018849265971338186 + }, + { + "days": 1.352083, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.352778, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.353472, + "weight": 9.670492976599591e-05 + }, + { + "days": 1.354167, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.354861, + "weight": 6.228453103572619e-05 + }, + { + "days": 1.355556, + "weight": 9.998306297840256e-05 + }, + { + "days": 1.357639, + "weight": 0.0001491550611645022 + }, + { + "days": 1.360417, + "weight": 0.0005228622473788593 + }, + { + "days": 1.361111, + "weight": 0.00010653932940321584 + }, + { + "days": 1.363194, + "weight": 0.00011309559582802913 + }, + { + "days": 1.372222, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.372917, + "weight": 0.00011637372904043576 + }, + { + "days": 1.377083, + "weight": 5.245013139850626e-05 + }, + { + "days": 1.379861, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.382639, + "weight": 9.342679655358928e-05 + }, + { + "days": 1.385417, + "weight": 6.556266424813282e-05 + }, + { + "days": 1.386111, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.386806, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.388194, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.388889, + "weight": 4.2615731761286336e-05 + }, + { + "days": 1.389583, + "weight": 8.359239691636936e-05 + }, + { + "days": 1.390972, + "weight": 6.720173085433615e-05 + }, + { + "days": 1.391667, + "weight": 0.0001032611961908092 + }, + { + "days": 1.392361, + "weight": 0.00012620812867765568 + }, + { + "days": 1.393056, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.395139, + "weight": 4.097666515508301e-05 + }, + { + "days": 1.396528, + "weight": 3.1142265517863095e-05 + }, + { + "days": 1.397222, + "weight": 0.00028847572269178446 + }, + { + "days": 1.397917, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.398611, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.399306, + "weight": 0.0004146838513694401 + }, + { + "days": 1.4, + "weight": 8.031426370396271e-05 + }, + { + "days": 1.402083, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.402778, + "weight": 2.786413230545645e-05 + }, + { + "days": 1.403472, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.404167, + "weight": 0.00030322732214761435 + }, + { + "days": 1.405556, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.407639, + "weight": 0.00010981746261562248 + }, + { + "days": 1.409028, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.409722, + "weight": 3.1142265517863095e-05 + }, + { + "days": 1.410417, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.4125, + "weight": 0.0001491550611645022 + }, + { + "days": 1.413194, + "weight": 0.00011309559582802913 + }, + { + "days": 1.414583, + "weight": 0.0002229130584436516 + }, + { + "days": 1.415972, + "weight": 3.278133212406641e-05 + }, + { + "days": 1.418056, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.41875, + "weight": 5.408919800470958e-05 + }, + { + "days": 1.419444, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.420139, + "weight": 0.00034420398730269734 + }, + { + "days": 1.426389, + "weight": 0.00010817839600941917 + }, + { + "days": 1.430556, + "weight": 8.850959673497932e-05 + }, + { + "days": 1.435417, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.4375, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.440278, + "weight": 4.2615731761286336e-05 + }, + { + "days": 1.441667, + "weight": 4.097666515508301e-05 + }, + { + "days": 1.445833, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.447222, + "weight": 4.2615731761286336e-05 + }, + { + "days": 1.448611, + "weight": 0.0002851975894793778 + }, + { + "days": 1.449306, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.45, + "weight": 5.081106479230294e-05 + }, + { + "days": 1.451389, + "weight": 0.0001425987947396889 + }, + { + "days": 1.453472, + "weight": 7.211893067294611e-05 + }, + { + "days": 1.454167, + "weight": 0.00019832705935060179 + }, + { + "days": 1.45625, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.456944, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.458333, + "weight": 0.00013112532849626565 + }, + { + "days": 1.460417, + "weight": 0.0002950319891165977 + }, + { + "days": 1.461111, + "weight": 0.00026388972359873465 + }, + { + "days": 1.463889, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.464583, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.465972, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.468056, + "weight": 6.228453103572619e-05 + }, + { + "days": 1.469444, + "weight": 6.228453103572619e-05 + }, + { + "days": 1.470139, + "weight": 0.00021307865880643167 + }, + { + "days": 1.471528, + "weight": 0.0001360425283148756 + }, + { + "days": 1.472222, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.472917, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.474306, + "weight": 5.408919800470958e-05 + }, + { + "days": 1.475694, + "weight": 7.539706388535275e-05 + }, + { + "days": 1.476389, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.477778, + "weight": 9.342679655358928e-05 + }, + { + "days": 1.479167, + "weight": 4.097666515508301e-05 + }, + { + "days": 1.48125, + "weight": 0.00038681971906398365 + }, + { + "days": 1.482639, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.495833, + "weight": 6.720173085433615e-05 + }, + { + "days": 1.498611, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.499306, + "weight": 0.00019177079292578852 + }, + { + "days": 1.504167, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.507639, + "weight": 8.523146352257267e-05 + }, + { + "days": 1.508333, + "weight": 8.359239691636936e-05 + }, + { + "days": 1.509028, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.509722, + "weight": 9.670492976599591e-05 + }, + { + "days": 1.510417, + "weight": 3.1142265517863095e-05 + }, + { + "days": 1.513194, + "weight": 0.00010817839600941917 + }, + { + "days": 1.513889, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.515278, + "weight": 3.278133212406641e-05 + }, + { + "days": 1.518056, + "weight": 6.556266424813282e-05 + }, + { + "days": 1.51875, + "weight": 4.097666515508301e-05 + }, + { + "days": 1.519444, + "weight": 0.00010981746261562248 + }, + { + "days": 1.520139, + "weight": 0.0004441870502810999 + }, + { + "days": 1.521528, + "weight": 0.00010162212958460587 + }, + { + "days": 1.522222, + "weight": 0.00015735039419551879 + }, + { + "days": 1.523611, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.524306, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.527083, + "weight": 0.00012292999546524905 + }, + { + "days": 1.527778, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.528472, + "weight": 7.047986406674279e-05 + }, + { + "days": 1.529167, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.530556, + "weight": 4.2615731761286336e-05 + }, + { + "days": 1.53125, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.532639, + "weight": 0.00012129092885904572 + }, + { + "days": 1.534028, + "weight": 6.720173085433615e-05 + }, + { + "days": 1.534722, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.535417, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.536111, + "weight": 7.047986406674279e-05 + }, + { + "days": 1.536806, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.538194, + "weight": 0.0004130447847632368 + }, + { + "days": 1.545833, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.550694, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.55625, + "weight": 4.097666515508301e-05 + }, + { + "days": 1.558333, + "weight": 0.000388458785670187 + }, + { + "days": 1.564583, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.565972, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.567361, + "weight": 5.081106479230294e-05 + }, + { + "days": 1.568056, + "weight": 4.097666515508301e-05 + }, + { + "days": 1.570139, + "weight": 4.097666515508301e-05 + }, + { + "days": 1.572917, + "weight": 0.0001344034617086723 + }, + { + "days": 1.574306, + "weight": 7.211893067294611e-05 + }, + { + "days": 1.577778, + "weight": 4.2615731761286336e-05 + }, + { + "days": 1.578472, + "weight": 4.75329315798963e-05 + }, + { + "days": 1.579167, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.580556, + "weight": 0.00013932066152728227 + }, + { + "days": 1.581944, + "weight": 0.0004556605165245231 + }, + { + "days": 1.582639, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.583333, + "weight": 7.211893067294611e-05 + }, + { + "days": 1.585417, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.586806, + "weight": 3.278133212406641e-05 + }, + { + "days": 1.5875, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.588194, + "weight": 0.00014423786134589223 + }, + { + "days": 1.590972, + "weight": 9.50658631597926e-05 + }, + { + "days": 1.592361, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.593056, + "weight": 8.195333031016603e-05 + }, + { + "days": 1.594444, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.595833, + "weight": 0.00037206811960815376 + }, + { + "days": 1.596528, + "weight": 7.047986406674279e-05 + }, + { + "days": 1.597917, + "weight": 7.211893067294611e-05 + }, + { + "days": 1.599306, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.6, + "weight": 3.1142265517863095e-05 + }, + { + "days": 1.601389, + "weight": 2.786413230545645e-05 + }, + { + "days": 1.615278, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.621528, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.625, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.627083, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.634722, + "weight": 9.50658631597926e-05 + }, + { + "days": 1.6375, + "weight": 3.278133212406641e-05 + }, + { + "days": 1.639583, + "weight": 6.720173085433615e-05 + }, + { + "days": 1.640972, + "weight": 7.047986406674279e-05 + }, + { + "days": 1.641667, + "weight": 3.1142265517863095e-05 + }, + { + "days": 1.642361, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.64375, + "weight": 0.00015407226098311215 + }, + { + "days": 1.644444, + "weight": 8.195333031016603e-05 + }, + { + "days": 1.645833, + "weight": 6.228453103572619e-05 + }, + { + "days": 1.646528, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.647917, + "weight": 0.00020160519256300844 + }, + { + "days": 1.650694, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.653472, + "weight": 9.178772994738595e-05 + }, + { + "days": 1.654861, + "weight": 0.00020160519256300844 + }, + { + "days": 1.655556, + "weight": 0.0001507941277707055 + }, + { + "days": 1.65625, + "weight": 5.408919800470958e-05 + }, + { + "days": 1.656944, + "weight": 0.00010981746261562248 + }, + { + "days": 1.658333, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.659722, + "weight": 0.00043271358403767666 + }, + { + "days": 1.6625, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.668056, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.670833, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.671528, + "weight": 0.00012620812867765568 + }, + { + "days": 1.676389, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.684028, + "weight": 3.1142265517863095e-05 + }, + { + "days": 1.689583, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.691667, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.69375, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.696528, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.697917, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.698611, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.699306, + "weight": 8.523146352257267e-05 + }, + { + "days": 1.700694, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.703472, + "weight": 9.670492976599591e-05 + }, + { + "days": 1.704861, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.705556, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.709722, + "weight": 0.00017865826007616194 + }, + { + "days": 1.711806, + "weight": 0.00010981746261562248 + }, + { + "days": 1.7125, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.713889, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.714583, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.715278, + "weight": 3.1142265517863095e-05 + }, + { + "days": 1.715972, + "weight": 9.342679655358928e-05 + }, + { + "days": 1.716667, + "weight": 0.00017210199365134867 + }, + { + "days": 1.71875, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.720139, + "weight": 0.0007129939736984444 + }, + { + "days": 1.721528, + "weight": 5.9006397823319545e-05 + }, + { + "days": 1.731944, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.735417, + "weight": 0.00010490026279701252 + }, + { + "days": 1.738889, + "weight": 6.720173085433615e-05 + }, + { + "days": 1.743056, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.744444, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.748611, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.757639, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.758333, + "weight": 6.556266424813282e-05 + }, + { + "days": 1.760417, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.761111, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.7625, + "weight": 0.00019504892613819515 + }, + { + "days": 1.763194, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.766667, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.772222, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.773611, + "weight": 0.00013112532849626565 + }, + { + "days": 1.775, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.775694, + "weight": 9.178772994738595e-05 + }, + { + "days": 1.777083, + "weight": 8.687053012877599e-05 + }, + { + "days": 1.777778, + "weight": 9.014866334118263e-05 + }, + { + "days": 1.779167, + "weight": 8.850959673497932e-05 + }, + { + "days": 1.781944, + "weight": 0.00018029732668236527 + }, + { + "days": 1.782639, + "weight": 0.0006113718441138386 + }, + { + "days": 1.789583, + "weight": 0.00011473466243423244 + }, + { + "days": 1.810417, + "weight": 5.408919800470958e-05 + }, + { + "days": 1.813194, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.815278, + "weight": 3.605946533647306e-05 + }, + { + "days": 1.819444, + "weight": 9.998306297840256e-05 + }, + { + "days": 1.820833, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.821528, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.823611, + "weight": 8.195333031016603e-05 + }, + { + "days": 1.825, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.825694, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.827083, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.828472, + "weight": 8.850959673497932e-05 + }, + { + "days": 1.829167, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.83125, + "weight": 5.245013139850626e-05 + }, + { + "days": 1.831944, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.834028, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.834722, + "weight": 0.00010162212958460587 + }, + { + "days": 1.835417, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.836806, + "weight": 0.0001835754598947719 + }, + { + "days": 1.8375, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.838889, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.839583, + "weight": 4.75329315798963e-05 + }, + { + "days": 1.840278, + "weight": 8.359239691636936e-05 + }, + { + "days": 1.840972, + "weight": 0.0001425987947396889 + }, + { + "days": 1.844444, + "weight": 0.000634318776600685 + }, + { + "days": 1.845833, + "weight": 4.2615731761286336e-05 + }, + { + "days": 1.847917, + "weight": 7.539706388535275e-05 + }, + { + "days": 1.849306, + "weight": 5.245013139850626e-05 + }, + { + "days": 1.85625, + "weight": 8.850959673497932e-05 + }, + { + "days": 1.857639, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.863889, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.867361, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.868056, + "weight": 5.245013139850626e-05 + }, + { + "days": 1.872222, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.872917, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.878472, + "weight": 0.00010490026279701252 + }, + { + "days": 1.880556, + "weight": 4.425479836748966e-05 + }, + { + "days": 1.88125, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.882639, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.883333, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.884722, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.885417, + "weight": 3.278133212406641e-05 + }, + { + "days": 1.886806, + "weight": 3.278133212406641e-05 + }, + { + "days": 1.888194, + "weight": 6.064546442952286e-05 + }, + { + "days": 1.890278, + "weight": 0.00025241625735531137 + }, + { + "days": 1.891667, + "weight": 0.00010162212958460587 + }, + { + "days": 1.894444, + "weight": 5.245013139850626e-05 + }, + { + "days": 1.895833, + "weight": 8.195333031016603e-05 + }, + { + "days": 1.897222, + "weight": 7.047986406674279e-05 + }, + { + "days": 1.897917, + "weight": 0.00025241625735531137 + }, + { + "days": 1.898611, + "weight": 8.195333031016603e-05 + }, + { + "days": 1.899306, + "weight": 0.00017374106025755198 + }, + { + "days": 1.9, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.900694, + "weight": 0.00012456906207145238 + }, + { + "days": 1.906944, + "weight": 9.998306297840256e-05 + }, + { + "days": 1.907639, + "weight": 0.0009834399637219924 + }, + { + "days": 1.908333, + "weight": 5.081106479230294e-05 + }, + { + "days": 1.909722, + "weight": 5.245013139850626e-05 + }, + { + "days": 1.916667, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.921528, + "weight": 2.786413230545645e-05 + }, + { + "days": 1.922917, + "weight": 7.047986406674279e-05 + }, + { + "days": 1.923611, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.925694, + "weight": 3.1142265517863095e-05 + }, + { + "days": 1.932639, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.933333, + "weight": 0.00012129092885904572 + }, + { + "days": 1.936111, + "weight": 4.097666515508301e-05 + }, + { + "days": 1.939583, + "weight": 5.245013139850626e-05 + }, + { + "days": 1.940278, + "weight": 0.00011473466243423244 + }, + { + "days": 1.941667, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.942361, + "weight": 9.670492976599591e-05 + }, + { + "days": 1.943056, + "weight": 5.408919800470958e-05 + }, + { + "days": 1.945139, + "weight": 5.408919800470958e-05 + }, + { + "days": 1.945833, + "weight": 0.00010162212958460587 + }, + { + "days": 1.946528, + "weight": 0.00016062852740792542 + }, + { + "days": 1.947222, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.947917, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.949306, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.95, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.950694, + "weight": 9.178772994738595e-05 + }, + { + "days": 1.951389, + "weight": 9.998306297840256e-05 + }, + { + "days": 1.954861, + "weight": 0.00021143959220022837 + }, + { + "days": 1.955556, + "weight": 3.7698531942676374e-05 + }, + { + "days": 1.956944, + "weight": 0.00014423786134589223 + }, + { + "days": 1.958333, + "weight": 6.228453103572619e-05 + }, + { + "days": 1.959028, + "weight": 0.0004163229179756434 + }, + { + "days": 1.959722, + "weight": 0.000168823860438942 + }, + { + "days": 1.961111, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.963194, + "weight": 2.9503198911659772e-05 + }, + { + "days": 1.963889, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.964583, + "weight": 0.0002081614589878217 + }, + { + "days": 1.965972, + "weight": 0.00041140571815703346 + }, + { + "days": 1.966667, + "weight": 4.917199818609962e-05 + }, + { + "days": 1.968056, + "weight": 4.75329315798963e-05 + }, + { + "days": 1.970139, + "weight": 5.57282646109129e-05 + }, + { + "days": 1.970833, + "weight": 4.75329315798963e-05 + }, + { + "days": 1.972917, + "weight": 9.50658631597926e-05 + }, + { + "days": 1.975694, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.98125, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.981944, + "weight": 7.375799727914943e-05 + }, + { + "days": 1.982639, + "weight": 0.0001819363932885686 + }, + { + "days": 1.9875, + "weight": 9.50658631597926e-05 + }, + { + "days": 1.988194, + "weight": 9.998306297840256e-05 + }, + { + "days": 1.990972, + "weight": 6.064546442952286e-05 + }, + { + "days": 1.99375, + "weight": 0.00010817839600941917 + }, + { + "days": 1.997222, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.997917, + "weight": 6.392359764192951e-05 + }, + { + "days": 1.998611, + "weight": 0.00025241625735531137 + }, + { + "days": 1.999306, + "weight": 8.523146352257267e-05 + }, + { + "days": 2.001389, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.002083, + "weight": 0.00011637372904043576 + }, + { + "days": 2.004167, + "weight": 0.00038354158585157704 + }, + { + "days": 2.004861, + "weight": 0.00019668799274439848 + }, + { + "days": 2.005556, + "weight": 3.4420398730269734e-05 + }, + { + "days": 2.006944, + "weight": 3.605946533647306e-05 + }, + { + "days": 2.008333, + "weight": 8.195333031016603e-05 + }, + { + "days": 2.009028, + "weight": 0.00019340985953199182 + }, + { + "days": 2.009722, + "weight": 0.0001671847938327387 + }, + { + "days": 2.010417, + "weight": 0.00013768159492107894 + }, + { + "days": 2.013194, + "weight": 4.097666515508301e-05 + }, + { + "days": 2.014583, + "weight": 6.884079746053947e-05 + }, + { + "days": 2.015278, + "weight": 5.408919800470958e-05 + }, + { + "days": 2.015972, + "weight": 0.0002622506569925313 + }, + { + "days": 2.018056, + "weight": 5.9006397823319545e-05 + }, + { + "days": 2.01875, + "weight": 9.834399637219924e-05 + }, + { + "days": 2.020139, + "weight": 0.00012129092885904572 + }, + { + "days": 2.020833, + "weight": 0.000342564920696494 + }, + { + "days": 2.023611, + "weight": 0.00012129092885904572 + }, + { + "days": 2.024306, + "weight": 6.064546442952286e-05 + }, + { + "days": 2.025694, + "weight": 0.0005130278477416394 + }, + { + "days": 2.03125, + "weight": 0.00030650545536002095 + }, + { + "days": 2.031944, + "weight": 5.736733121711622e-05 + }, + { + "days": 2.034028, + "weight": 9.342679655358928e-05 + }, + { + "days": 2.041667, + "weight": 6.392359764192951e-05 + }, + { + "days": 2.042361, + "weight": 9.342679655358928e-05 + }, + { + "days": 2.044444, + "weight": 4.917199818609962e-05 + }, + { + "days": 2.045139, + "weight": 5.081106479230294e-05 + }, + { + "days": 2.046528, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.048611, + "weight": 4.75329315798963e-05 + }, + { + "days": 2.049306, + "weight": 7.375799727914943e-05 + }, + { + "days": 2.052778, + "weight": 8.687053012877599e-05 + }, + { + "days": 2.053472, + "weight": 4.917199818609962e-05 + }, + { + "days": 2.054167, + "weight": 3.605946533647306e-05 + }, + { + "days": 2.056944, + "weight": 9.998306297840256e-05 + }, + { + "days": 2.057639, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.059028, + "weight": 3.605946533647306e-05 + }, + { + "days": 2.059722, + "weight": 7.375799727914943e-05 + }, + { + "days": 2.063889, + "weight": 0.00011637372904043576 + }, + { + "days": 2.065278, + "weight": 6.064546442952286e-05 + }, + { + "days": 2.065972, + "weight": 3.605946533647306e-05 + }, + { + "days": 2.066667, + "weight": 7.047986406674279e-05 + }, + { + "days": 2.068056, + "weight": 0.00010817839600941917 + }, + { + "days": 2.070833, + "weight": 0.00016554572722653538 + }, + { + "days": 2.071528, + "weight": 6.392359764192951e-05 + }, + { + "days": 2.072917, + "weight": 7.867519709775939e-05 + }, + { + "days": 2.073611, + "weight": 4.917199818609962e-05 + }, + { + "days": 2.075, + "weight": 0.00016554572722653538 + }, + { + "days": 2.077083, + "weight": 0.00011801279564663909 + }, + { + "days": 2.078472, + "weight": 9.998306297840256e-05 + }, + { + "days": 2.079861, + "weight": 8.523146352257267e-05 + }, + { + "days": 2.080556, + "weight": 4.425479836748966e-05 + }, + { + "days": 2.082639, + "weight": 7.047986406674279e-05 + }, + { + "days": 2.083333, + "weight": 2.786413230545645e-05 + }, + { + "days": 2.086806, + "weight": 0.00036059465336473053 + }, + { + "days": 2.093056, + "weight": 0.0005081106479230295 + }, + { + "days": 2.095139, + "weight": 3.4420398730269734e-05 + }, + { + "days": 2.108333, + "weight": 7.867519709775939e-05 + }, + { + "days": 2.110417, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.115972, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.117361, + "weight": 2.786413230545645e-05 + }, + { + "days": 2.11875, + "weight": 0.00017374106025755198 + }, + { + "days": 2.120833, + "weight": 5.736733121711622e-05 + }, + { + "days": 2.122917, + "weight": 2.9503198911659772e-05 + }, + { + "days": 2.125, + "weight": 4.425479836748966e-05 + }, + { + "days": 2.126389, + "weight": 3.4420398730269734e-05 + }, + { + "days": 2.127778, + "weight": 6.556266424813282e-05 + }, + { + "days": 2.129861, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.130556, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.132639, + "weight": 2.9503198911659772e-05 + }, + { + "days": 2.134028, + "weight": 5.9006397823319545e-05 + }, + { + "days": 2.136111, + "weight": 0.00010490026279701252 + }, + { + "days": 2.1375, + "weight": 0.0001507941277707055 + }, + { + "days": 2.138194, + "weight": 8.523146352257267e-05 + }, + { + "days": 2.138889, + "weight": 4.2615731761286336e-05 + }, + { + "days": 2.140278, + "weight": 0.00015571132758931546 + }, + { + "days": 2.140972, + "weight": 4.75329315798963e-05 + }, + { + "days": 2.145139, + "weight": 4.2615731761286336e-05 + }, + { + "days": 2.15, + "weight": 0.00012292999546524905 + }, + { + "days": 2.152778, + "weight": 2.9503198911659772e-05 + }, + { + "days": 2.154861, + "weight": 0.00018685359310717856 + }, + { + "days": 2.159028, + "weight": 0.00010162212958460587 + }, + { + "days": 2.164583, + "weight": 3.605946533647306e-05 + }, + { + "days": 2.165972, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.168056, + "weight": 3.1142265517863095e-05 + }, + { + "days": 2.178472, + "weight": 2.786413230545645e-05 + }, + { + "days": 2.180556, + "weight": 5.9006397823319545e-05 + }, + { + "days": 2.181944, + "weight": 0.00010817839600941917 + }, + { + "days": 2.182639, + "weight": 5.408919800470958e-05 + }, + { + "days": 2.184722, + "weight": 0.00010981746261562248 + }, + { + "days": 2.188194, + "weight": 3.4420398730269734e-05 + }, + { + "days": 2.189583, + "weight": 6.064546442952286e-05 + }, + { + "days": 2.190972, + "weight": 6.556266424813282e-05 + }, + { + "days": 2.191667, + "weight": 3.605946533647306e-05 + }, + { + "days": 2.195139, + "weight": 6.228453103572619e-05 + }, + { + "days": 2.196528, + "weight": 2.9503198911659772e-05 + }, + { + "days": 2.197222, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.197917, + "weight": 3.605946533647306e-05 + }, + { + "days": 2.198611, + "weight": 6.720173085433615e-05 + }, + { + "days": 2.199306, + "weight": 5.57282646109129e-05 + }, + { + "days": 2.2, + "weight": 6.392359764192951e-05 + }, + { + "days": 2.204167, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.206944, + "weight": 0.00013932066152728227 + }, + { + "days": 2.210417, + "weight": 0.000260611590386328 + }, + { + "days": 2.211806, + "weight": 2.786413230545645e-05 + }, + { + "days": 2.220833, + "weight": 0.00013276439510246898 + }, + { + "days": 2.238889, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.241667, + "weight": 4.75329315798963e-05 + }, + { + "days": 2.24375, + "weight": 6.720173085433615e-05 + }, + { + "days": 2.245139, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.246528, + "weight": 5.081106479230294e-05 + }, + { + "days": 2.247917, + "weight": 9.178772994738595e-05 + }, + { + "days": 2.250694, + "weight": 5.9006397823319545e-05 + }, + { + "days": 2.252778, + "weight": 5.245013139850626e-05 + }, + { + "days": 2.253472, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.254167, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.25625, + "weight": 7.867519709775939e-05 + }, + { + "days": 2.256944, + "weight": 0.00012784719528385901 + }, + { + "days": 2.258333, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.259028, + "weight": 5.081106479230294e-05 + }, + { + "days": 2.261806, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.266667, + "weight": 9.670492976599591e-05 + }, + { + "days": 2.269444, + "weight": 0.00021635679201883833 + }, + { + "days": 2.272222, + "weight": 0.00018029732668236527 + }, + { + "days": 2.299306, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.302778, + "weight": 5.57282646109129e-05 + }, + { + "days": 2.309722, + "weight": 4.2615731761286336e-05 + }, + { + "days": 2.311806, + "weight": 6.884079746053947e-05 + }, + { + "days": 2.3125, + "weight": 5.736733121711622e-05 + }, + { + "days": 2.313194, + "weight": 3.1142265517863095e-05 + }, + { + "days": 2.314583, + "weight": 6.064546442952286e-05 + }, + { + "days": 2.315972, + "weight": 6.064546442952286e-05 + }, + { + "days": 2.318056, + "weight": 0.00012292999546524905 + }, + { + "days": 2.31875, + "weight": 5.245013139850626e-05 + }, + { + "days": 2.325694, + "weight": 9.342679655358928e-05 + }, + { + "days": 2.330556, + "weight": 0.0002442209243242948 + }, + { + "days": 2.33125, + "weight": 0.00012129092885904572 + }, + { + "days": 2.336111, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.338194, + "weight": 0.00011801279564663909 + }, + { + "days": 2.360417, + "weight": 5.245013139850626e-05 + }, + { + "days": 2.36875, + "weight": 4.75329315798963e-05 + }, + { + "days": 2.373611, + "weight": 0.00010162212958460587 + }, + { + "days": 2.375, + "weight": 3.1142265517863095e-05 + }, + { + "days": 2.376389, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.379167, + "weight": 7.539706388535275e-05 + }, + { + "days": 2.379861, + "weight": 3.1142265517863095e-05 + }, + { + "days": 2.384722, + "weight": 0.00016554572722653538 + }, + { + "days": 2.385417, + "weight": 4.75329315798963e-05 + }, + { + "days": 2.386111, + "weight": 0.0002589725237801247 + }, + { + "days": 2.392361, + "weight": 0.00013932066152728227 + }, + { + "days": 2.39375, + "weight": 2.9503198911659772e-05 + }, + { + "days": 2.397222, + "weight": 7.703613049155608e-05 + }, + { + "days": 2.421528, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.43125, + "weight": 7.703613049155608e-05 + }, + { + "days": 2.432639, + "weight": 6.064546442952286e-05 + }, + { + "days": 2.434722, + "weight": 7.867519709775939e-05 + }, + { + "days": 2.435417, + "weight": 2.786413230545645e-05 + }, + { + "days": 2.436806, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.4375, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.440278, + "weight": 0.00017210199365134867 + }, + { + "days": 2.44375, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.447917, + "weight": 0.0001196518622528424 + }, + { + "days": 2.448611, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.450694, + "weight": 0.00026716785681114125 + }, + { + "days": 2.451389, + "weight": 5.57282646109129e-05 + }, + { + "days": 2.458333, + "weight": 0.0001360425283148756 + }, + { + "days": 2.478472, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.49375, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.495139, + "weight": 2.9503198911659772e-05 + }, + { + "days": 2.495833, + "weight": 7.211893067294611e-05 + }, + { + "days": 2.496528, + "weight": 6.228453103572619e-05 + }, + { + "days": 2.499306, + "weight": 0.00026716785681114125 + }, + { + "days": 2.501389, + "weight": 4.097666515508301e-05 + }, + { + "days": 2.505556, + "weight": 0.00035895558675852723 + }, + { + "days": 2.509722, + "weight": 6.392359764192951e-05 + }, + { + "days": 2.511111, + "weight": 4.917199818609962e-05 + }, + { + "days": 2.5125, + "weight": 0.00013768159492107894 + }, + { + "days": 2.513194, + "weight": 4.425479836748966e-05 + }, + { + "days": 2.513889, + "weight": 0.00010490026279701252 + }, + { + "days": 2.532639, + "weight": 3.605946533647306e-05 + }, + { + "days": 2.538194, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.552083, + "weight": 2.9503198911659772e-05 + }, + { + "days": 2.554167, + "weight": 5.081106479230294e-05 + }, + { + "days": 2.557639, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.561111, + "weight": 3.1142265517863095e-05 + }, + { + "days": 2.5625, + "weight": 5.9006397823319545e-05 + }, + { + "days": 2.565278, + "weight": 7.047986406674279e-05 + }, + { + "days": 2.565972, + "weight": 0.0004589386497369298 + }, + { + "days": 2.567361, + "weight": 0.0002065223923816184 + }, + { + "days": 2.568056, + "weight": 5.408919800470958e-05 + }, + { + "days": 2.570139, + "weight": 8.031426370396271e-05 + }, + { + "days": 2.570833, + "weight": 0.00019504892613819515 + }, + { + "days": 2.578472, + "weight": 0.00014587692795209553 + }, + { + "days": 2.602083, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.620139, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.621528, + "weight": 0.00017865826007616194 + }, + { + "days": 2.628472, + "weight": 0.0001819363932885686 + }, + { + "days": 2.629167, + "weight": 9.178772994738595e-05 + }, + { + "days": 2.629861, + "weight": 0.0001196518622528424 + }, + { + "days": 2.63125, + "weight": 6.228453103572619e-05 + }, + { + "days": 2.632639, + "weight": 0.00018029732668236527 + }, + { + "days": 2.633333, + "weight": 0.00015898946080172212 + }, + { + "days": 2.636806, + "weight": 0.000562199845927739 + }, + { + "days": 2.672917, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.674306, + "weight": 5.9006397823319545e-05 + }, + { + "days": 2.680556, + "weight": 0.0002753631898421579 + }, + { + "days": 2.684722, + "weight": 0.0001114565292218258 + }, + { + "days": 2.686806, + "weight": 8.195333031016603e-05 + }, + { + "days": 2.688194, + "weight": 0.00048680278204238624 + }, + { + "days": 2.689583, + "weight": 5.736733121711622e-05 + }, + { + "days": 2.690278, + "weight": 0.00013276439510246898 + }, + { + "days": 2.69375, + "weight": 3.605946533647306e-05 + }, + { + "days": 2.694444, + "weight": 9.178772994738595e-05 + }, + { + "days": 2.698611, + "weight": 0.00012948626189006234 + }, + { + "days": 2.700694, + "weight": 4.097666515508301e-05 + }, + { + "days": 2.713889, + "weight": 2.786413230545645e-05 + }, + { + "days": 2.729861, + "weight": 5.57282646109129e-05 + }, + { + "days": 2.731944, + "weight": 2.786413230545645e-05 + }, + { + "days": 2.739583, + "weight": 0.00020324425916921175 + }, + { + "days": 2.747222, + "weight": 0.0007113549070922411 + }, + { + "days": 2.748611, + "weight": 7.211893067294611e-05 + }, + { + "days": 2.75, + "weight": 8.195333031016603e-05 + }, + { + "days": 2.750694, + "weight": 5.245013139850626e-05 + }, + { + "days": 2.751389, + "weight": 5.408919800470958e-05 + }, + { + "days": 2.754861, + "weight": 0.00010981746261562248 + }, + { + "days": 2.75625, + "weight": 0.00017865826007616194 + }, + { + "days": 2.757639, + "weight": 2.9503198911659772e-05 + }, + { + "days": 2.780556, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.793056, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.794444, + "weight": 5.081106479230294e-05 + }, + { + "days": 2.804861, + "weight": 6.720173085433615e-05 + }, + { + "days": 2.805556, + "weight": 3.1142265517863095e-05 + }, + { + "days": 2.80625, + "weight": 0.0012915844856882168 + }, + { + "days": 2.807639, + "weight": 8.687053012877599e-05 + }, + { + "days": 2.809028, + "weight": 2.9503198911659772e-05 + }, + { + "days": 2.813889, + "weight": 0.00037206811960815376 + }, + { + "days": 2.816667, + "weight": 8.687053012877599e-05 + }, + { + "days": 2.822222, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.848611, + "weight": 5.736733121711622e-05 + }, + { + "days": 2.849306, + "weight": 5.736733121711622e-05 + }, + { + "days": 2.859028, + "weight": 3.1142265517863095e-05 + }, + { + "days": 2.864583, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.867361, + "weight": 7.539706388535275e-05 + }, + { + "days": 2.868056, + "weight": 4.75329315798963e-05 + }, + { + "days": 2.870833, + "weight": 7.703613049155608e-05 + }, + { + "days": 2.871528, + "weight": 0.0001753801268637553 + }, + { + "days": 2.873611, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.875, + "weight": 3.605946533647306e-05 + }, + { + "days": 2.875694, + "weight": 0.000296671055722801 + }, + { + "days": 2.880556, + "weight": 0.006189115505023738 + }, + { + "days": 2.882639, + "weight": 0.00014423786134589223 + }, + { + "days": 2.890972, + "weight": 6.720173085433615e-05 + }, + { + "days": 2.895833, + "weight": 5.736733121711622e-05 + }, + { + "days": 2.913194, + "weight": 0.0001032611961908092 + }, + { + "days": 2.914583, + "weight": 0.00015898946080172212 + }, + { + "days": 2.91875, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.919444, + "weight": 5.081106479230294e-05 + }, + { + "days": 2.922917, + "weight": 0.00017046292704514534 + }, + { + "days": 2.925694, + "weight": 0.00030322732214761435 + }, + { + "days": 2.929167, + "weight": 0.000260611590386328 + }, + { + "days": 2.934028, + "weight": 0.00024094279111188814 + }, + { + "days": 2.9375, + "weight": 6.556266424813282e-05 + }, + { + "days": 2.940972, + "weight": 0.0001360425283148756 + }, + { + "days": 2.941667, + "weight": 0.0008785397009249799 + }, + { + "days": 2.942361, + "weight": 0.003900978522763903 + }, + { + "days": 2.95, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.954167, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.957639, + "weight": 8.031426370396271e-05 + }, + { + "days": 2.959028, + "weight": 4.2615731761286336e-05 + }, + { + "days": 2.959722, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.960417, + "weight": 3.7698531942676374e-05 + }, + { + "days": 2.965278, + "weight": 3.1142265517863095e-05 + }, + { + "days": 2.965972, + "weight": 4.2615731761286336e-05 + }, + { + "days": 2.968056, + "weight": 0.0003491211871213073 + }, + { + "days": 2.969444, + "weight": 4.917199818609962e-05 + }, + { + "days": 2.971528, + "weight": 3.4420398730269734e-05 + }, + { + "days": 2.972222, + "weight": 0.0004196010511880501 + }, + { + "days": 2.972917, + "weight": 4.2615731761286336e-05 + }, + { + "days": 2.974306, + "weight": 0.00010981746261562248 + }, + { + "days": 2.976389, + "weight": 3.278133212406641e-05 + }, + { + "days": 2.977083, + "weight": 5.57282646109129e-05 + }, + { + "days": 2.978472, + "weight": 6.392359764192951e-05 + }, + { + "days": 2.982639, + "weight": 6.392359764192951e-05 + }, + { + "days": 2.984722, + "weight": 0.00024094279111188814 + }, + { + "days": 2.985417, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.986806, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.9875, + "weight": 0.00044746518349350655 + }, + { + "days": 2.990972, + "weight": 0.0007441362392163075 + }, + { + "days": 2.993056, + "weight": 0.000562199845927739 + }, + { + "days": 2.995139, + "weight": 0.000521223180772656 + }, + { + "days": 2.997917, + "weight": 0.0002212739918374483 + }, + { + "days": 3.0, + "weight": 0.0004212401177942534 + }, + { + "days": 3.008333, + "weight": 0.0022143789849806863 + }, + { + "days": 3.011111, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.015278, + "weight": 8.031426370396271e-05 + }, + { + "days": 3.017361, + "weight": 5.57282646109129e-05 + }, + { + "days": 3.030556, + "weight": 6.392359764192951e-05 + }, + { + "days": 3.031944, + "weight": 4.75329315798963e-05 + }, + { + "days": 3.032639, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.033333, + "weight": 0.00016554572722653538 + }, + { + "days": 3.036111, + "weight": 0.00013112532849626565 + }, + { + "days": 3.0375, + "weight": 5.245013139850626e-05 + }, + { + "days": 3.038194, + "weight": 0.00029011478929798776 + }, + { + "days": 3.042361, + "weight": 0.0005998983778704154 + }, + { + "days": 3.04375, + "weight": 8.031426370396271e-05 + }, + { + "days": 3.048611, + "weight": 0.00044746518349350655 + }, + { + "days": 3.049306, + "weight": 0.00012784719528385901 + }, + { + "days": 3.050694, + "weight": 0.00031306172178483427 + }, + { + "days": 3.052083, + "weight": 2.9503198911659772e-05 + }, + { + "days": 3.055556, + "weight": 6.392359764192951e-05 + }, + { + "days": 3.056944, + "weight": 8.523146352257267e-05 + }, + { + "days": 3.059722, + "weight": 0.0003737071862143571 + }, + { + "days": 3.061111, + "weight": 0.00021143959220022837 + }, + { + "days": 3.090972, + "weight": 8.523146352257267e-05 + }, + { + "days": 3.095139, + "weight": 3.1142265517863095e-05 + }, + { + "days": 3.098611, + "weight": 3.4420398730269734e-05 + }, + { + "days": 3.104167, + "weight": 4.75329315798963e-05 + }, + { + "days": 3.105556, + "weight": 3.9337598548879697e-05 + }, + { + "days": 3.106944, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.107639, + "weight": 0.00017374106025755198 + }, + { + "days": 3.109028, + "weight": 5.408919800470958e-05 + }, + { + "days": 3.1125, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.114583, + "weight": 0.00013768159492107894 + }, + { + "days": 3.115278, + "weight": 6.556266424813282e-05 + }, + { + "days": 3.115972, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.116667, + "weight": 6.884079746053947e-05 + }, + { + "days": 3.123611, + "weight": 6.884079746053947e-05 + }, + { + "days": 3.138194, + "weight": 3.1142265517863095e-05 + }, + { + "days": 3.15, + "weight": 3.4420398730269734e-05 + }, + { + "days": 3.156944, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.157639, + "weight": 4.2615731761286336e-05 + }, + { + "days": 3.165278, + "weight": 7.539706388535275e-05 + }, + { + "days": 3.169444, + "weight": 5.081106479230294e-05 + }, + { + "days": 3.170139, + "weight": 4.2615731761286336e-05 + }, + { + "days": 3.174306, + "weight": 3.4420398730269734e-05 + }, + { + "days": 3.175, + "weight": 7.375799727914943e-05 + }, + { + "days": 3.177778, + "weight": 2.9503198911659772e-05 + }, + { + "days": 3.18125, + "weight": 0.00012620812867765568 + }, + { + "days": 3.213194, + "weight": 0.000168823860438942 + }, + { + "days": 3.224306, + "weight": 9.998306297840256e-05 + }, + { + "days": 3.23125, + "weight": 5.57282646109129e-05 + }, + { + "days": 3.233333, + "weight": 3.4420398730269734e-05 + }, + { + "days": 3.234028, + "weight": 4.917199818609962e-05 + }, + { + "days": 3.235417, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.236111, + "weight": 2.9503198911659772e-05 + }, + { + "days": 3.275, + "weight": 5.245013139850626e-05 + }, + { + "days": 3.2875, + "weight": 9.998306297840256e-05 + }, + { + "days": 3.291667, + "weight": 3.1142265517863095e-05 + }, + { + "days": 3.293056, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.297917, + "weight": 2.9503198911659772e-05 + }, + { + "days": 3.300694, + "weight": 0.0001196518622528424 + }, + { + "days": 3.343056, + "weight": 6.064546442952286e-05 + }, + { + "days": 3.347222, + "weight": 4.917199818609962e-05 + }, + { + "days": 3.349306, + "weight": 5.245013139850626e-05 + }, + { + "days": 3.359028, + "weight": 3.4420398730269734e-05 + }, + { + "days": 3.404167, + "weight": 3.7698531942676374e-05 + }, + { + "days": 3.406944, + "weight": 3.1142265517863095e-05 + }, + { + "days": 3.409028, + "weight": 3.605946533647306e-05 + }, + { + "days": 3.417361, + "weight": 9.014866334118263e-05 + }, + { + "days": 3.419444, + "weight": 4.75329315798963e-05 + }, + { + "days": 3.478472, + "weight": 4.5893864973692975e-05 + }, + { + "days": 3.479167, + "weight": 3.4420398730269734e-05 + }, + { + "days": 3.485417, + "weight": 4.425479836748966e-05 + }, + { + "days": 3.524306, + "weight": 3.1142265517863095e-05 + }, + { + "days": 3.533333, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.544444, + "weight": 4.917199818609962e-05 + }, + { + "days": 3.545139, + "weight": 8.687053012877599e-05 + }, + { + "days": 3.590278, + "weight": 4.097666515508301e-05 + }, + { + "days": 3.611111, + "weight": 9.014866334118263e-05 + }, + { + "days": 3.660417, + "weight": 4.2615731761286336e-05 + }, + { + "days": 3.670139, + "weight": 0.00016390666062033205 + }, + { + "days": 3.719444, + "weight": 5.408919800470958e-05 + }, + { + "days": 3.723611, + "weight": 3.1142265517863095e-05 + }, + { + "days": 3.727778, + "weight": 0.00019340985953199182 + }, + { + "days": 3.757639, + "weight": 3.7698531942676374e-05 + }, + { + "days": 3.767361, + "weight": 4.2615731761286336e-05 + }, + { + "days": 3.779167, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.782639, + "weight": 2.9503198911659772e-05 + }, + { + "days": 3.784722, + "weight": 4.2615731761286336e-05 + }, + { + "days": 3.786111, + "weight": 0.00021799585862504163 + }, + { + "days": 3.798611, + "weight": 2.786413230545645e-05 + }, + { + "days": 3.804861, + "weight": 3.1142265517863095e-05 + }, + { + "days": 3.809028, + "weight": 4.425479836748966e-05 + }, + { + "days": 3.822222, + "weight": 4.75329315798963e-05 + }, + { + "days": 3.831944, + "weight": 4.097666515508301e-05 + }, + { + "days": 3.839583, + "weight": 3.1142265517863095e-05 + }, + { + "days": 3.840278, + "weight": 3.278133212406641e-05 + }, + { + "days": 3.849306, + "weight": 0.00042451825100666004 + }, + { + "days": 3.857639, + "weight": 4.5893864973692975e-05 + }, + { + "days": 3.86875, + "weight": 3.605946533647306e-05 + }, + { + "days": 3.884028, + "weight": 4.425479836748966e-05 + }, + { + "days": 3.89375, + "weight": 4.5893864973692975e-05 + }, + { + "days": 3.898611, + "weight": 0.0008310067693450835 + }, + { + "days": 3.899306, + "weight": 3.1142265517863095e-05 + }, + { + "days": 3.927083, + "weight": 8.195333031016603e-05 + }, + { + "days": 3.930556, + "weight": 6.228453103572619e-05 + }, + { + "days": 3.931944, + "weight": 0.0001753801268637553 + }, + { + "days": 3.932639, + "weight": 5.57282646109129e-05 + }, + { + "days": 3.943056, + "weight": 6.720173085433615e-05 + }, + { + "days": 3.94375, + "weight": 3.605946533647306e-05 + }, + { + "days": 3.945833, + "weight": 6.228453103572619e-05 + }, + { + "days": 3.952083, + "weight": 3.4420398730269734e-05 + }, + { + "days": 3.952778, + "weight": 0.0001114565292218258 + }, + { + "days": 3.953472, + "weight": 5.081106479230294e-05 + }, + { + "days": 3.95625, + "weight": 3.9337598548879697e-05 + }, + { + "days": 3.959722, + "weight": 0.00012129092885904572 + }, + { + "days": 3.961111, + "weight": 3.9337598548879697e-05 + }, + { + "days": 3.963889, + "weight": 8.031426370396271e-05 + }, + { + "days": 3.965278, + "weight": 0.00039665411870120357 + }, + { + "days": 3.968056, + "weight": 3.605946533647306e-05 + }, + { + "days": 3.984028, + "weight": 3.605946533647306e-05 + }, + { + "days": 3.99375, + "weight": 0.00015735039419551879 + }, + { + "days": 3.995833, + "weight": 8.359239691636936e-05 + }, + { + "days": 3.996528, + "weight": 6.064546442952286e-05 + }, + { + "days": 3.998611, + "weight": 8.031426370396271e-05 + }, + { + "days": 4.001389, + "weight": 0.00019504892613819515 + }, + { + "days": 4.002778, + "weight": 3.605946533647306e-05 + }, + { + "days": 4.004167, + "weight": 4.5893864973692975e-05 + }, + { + "days": 4.00625, + "weight": 3.605946533647306e-05 + }, + { + "days": 4.007639, + "weight": 7.047986406674279e-05 + }, + { + "days": 4.008333, + "weight": 3.278133212406641e-05 + }, + { + "days": 4.011806, + "weight": 0.00013112532849626565 + }, + { + "days": 4.015278, + "weight": 6.392359764192951e-05 + }, + { + "days": 4.018056, + "weight": 5.57282646109129e-05 + }, + { + "days": 4.01875, + "weight": 6.392359764192951e-05 + }, + { + "days": 4.019444, + "weight": 0.00021963492523124496 + }, + { + "days": 4.020833, + "weight": 0.00011309559582802913 + }, + { + "days": 4.021528, + "weight": 4.5893864973692975e-05 + }, + { + "days": 4.025, + "weight": 0.0001753801268637553 + }, + { + "days": 4.029861, + "weight": 3.4420398730269734e-05 + }, + { + "days": 4.035417, + "weight": 3.7698531942676374e-05 + }, + { + "days": 4.047917, + "weight": 5.408919800470958e-05 + }, + { + "days": 4.054167, + "weight": 3.605946533647306e-05 + }, + { + "days": 4.059722, + "weight": 5.245013139850626e-05 + }, + { + "days": 4.063194, + "weight": 8.195333031016603e-05 + }, + { + "days": 4.069444, + "weight": 2.786413230545645e-05 + }, + { + "days": 4.070139, + "weight": 6.720173085433615e-05 + }, + { + "days": 4.074306, + "weight": 8.195333031016603e-05 + }, + { + "days": 4.078472, + "weight": 3.1142265517863095e-05 + }, + { + "days": 4.079861, + "weight": 3.9337598548879697e-05 + }, + { + "days": 4.080556, + "weight": 3.1142265517863095e-05 + }, + { + "days": 4.082639, + "weight": 0.00012292999546524905 + }, + { + "days": 4.095833, + "weight": 2.9503198911659772e-05 + }, + { + "days": 4.11875, + "weight": 3.1142265517863095e-05 + }, + { + "days": 4.127083, + "weight": 5.9006397823319545e-05 + }, + { + "days": 4.129167, + "weight": 3.9337598548879697e-05 + }, + { + "days": 4.131944, + "weight": 5.081106479230294e-05 + }, + { + "days": 4.1375, + "weight": 4.097666515508301e-05 + }, + { + "days": 4.148611, + "weight": 8.523146352257267e-05 + }, + { + "days": 4.181944, + "weight": 4.75329315798963e-05 + }, + { + "days": 4.209028, + "weight": 3.1142265517863095e-05 + }, + { + "days": 4.211806, + "weight": 3.4420398730269734e-05 + }, + { + "days": 4.23125, + "weight": 4.917199818609962e-05 + }, + { + "days": 4.268056, + "weight": 2.9503198911659772e-05 + }, + { + "days": 4.273611, + "weight": 3.278133212406641e-05 + }, + { + "days": 4.305556, + "weight": 2.9503198911659772e-05 + }, + { + "days": 4.334722, + "weight": 9.670492976599591e-05 + }, + { + "days": 4.438889, + "weight": 3.605946533647306e-05 + }, + { + "days": 4.450694, + "weight": 2.786413230545645e-05 + }, + { + "days": 4.467361, + "weight": 3.278133212406641e-05 + }, + { + "days": 4.619444, + "weight": 4.917199818609962e-05 + }, + { + "days": 4.731944, + "weight": 3.1142265517863095e-05 + }, + { + "days": 4.760417, + "weight": 3.278133212406641e-05 + }, + { + "days": 4.770833, + "weight": 0.00020160519256300844 + }, + { + "days": 4.801389, + "weight": 3.1142265517863095e-05 + }, + { + "days": 4.827083, + "weight": 0.00015407226098311215 + }, + { + "days": 4.884722, + "weight": 0.0002622506569925313 + }, + { + "days": 4.886806, + "weight": 2.9503198911659772e-05 + }, + { + "days": 4.900694, + "weight": 3.605946533647306e-05 + }, + { + "days": 4.940278, + "weight": 2.9503198911659772e-05 + }, + { + "days": 4.942361, + "weight": 6.884079746053947e-05 + }, + { + "days": 4.95, + "weight": 9.998306297840256e-05 + }, + { + "days": 4.951389, + "weight": 0.0002622506569925313 + }, + { + "days": 4.952083, + "weight": 2.9503198911659772e-05 + }, + { + "days": 4.952778, + "weight": 2.9503198911659772e-05 + }, + { + "days": 4.963889, + "weight": 4.2615731761286336e-05 + }, + { + "days": 4.970139, + "weight": 0.000168823860438942 + }, + { + "days": 4.978472, + "weight": 3.605946533647306e-05 + }, + { + "days": 4.9875, + "weight": 6.720173085433615e-05 + }, + { + "days": 4.99375, + "weight": 3.278133212406641e-05 + }, + { + "days": 5.0, + "weight": 2.786413230545645e-05 + }, + { + "days": 5.00625, + "weight": 3.4420398730269734e-05 + }, + { + "days": 5.007639, + "weight": 4.425479836748966e-05 + }, + { + "days": 5.011806, + "weight": 4.425479836748966e-05 + }, + { + "days": 5.016667, + "weight": 4.2615731761286336e-05 + }, + { + "days": 5.018056, + "weight": 0.00010653932940321584 + }, + { + "days": 5.019444, + "weight": 7.047986406674279e-05 + }, + { + "days": 5.027778, + "weight": 3.605946533647306e-05 + }, + { + "days": 5.03125, + "weight": 3.278133212406641e-05 + }, + { + "days": 5.054861, + "weight": 7.375799727914943e-05 + }, + { + "days": 5.078472, + "weight": 5.081106479230294e-05 + }, + { + "days": 5.086111, + "weight": 3.605946533647306e-05 + }, + { + "days": 5.095833, + "weight": 3.9337598548879697e-05 + }, + { + "days": 5.097917, + "weight": 2.9503198911659772e-05 + }, + { + "days": 5.103472, + "weight": 6.392359764192951e-05 + }, + { + "days": 5.114583, + "weight": 3.4420398730269734e-05 + }, + { + "days": 5.13125, + "weight": 3.278133212406641e-05 + }, + { + "days": 5.771528, + "weight": 6.392359764192951e-05 + }, + { + "days": 5.838194, + "weight": 9.50658631597926e-05 + }, + { + "days": 5.891667, + "weight": 3.9337598548879697e-05 + }, + { + "days": 5.91875, + "weight": 0.0001507941277707055 + }, + { + "days": 5.936111, + "weight": 4.917199818609962e-05 + }, + { + "days": 5.947917, + "weight": 3.1142265517863095e-05 + }, + { + "days": 5.986111, + "weight": 7.867519709775939e-05 + }, + { + "days": 5.997222, + "weight": 3.9337598548879697e-05 + }, + { + "days": 5.998611, + "weight": 3.278133212406641e-05 + }, + { + "days": 6.002778, + "weight": 4.917199818609962e-05 + }, + { + "days": 6.00625, + "weight": 4.425479836748966e-05 + }, + { + "days": 6.017361, + "weight": 4.097666515508301e-05 + }, + { + "days": 6.04375, + "weight": 5.736733121711622e-05 + }, + { + "days": 6.124306, + "weight": 3.9337598548879697e-05 + }, + { + "days": 6.765972, + "weight": 3.7698531942676374e-05 + }, + { + "days": 6.844444, + "weight": 6.884079746053947e-05 + }, + { + "days": 6.902083, + "weight": 7.047986406674279e-05 + }, + { + "days": 6.947917, + "weight": 3.278133212406641e-05 + }, + { + "days": 6.963194, + "weight": 7.375799727914943e-05 + }, + { + "days": 6.970833, + "weight": 3.605946533647306e-05 + }, + { + "days": 7.010417, + "weight": 5.736733121711622e-05 + }, + { + "days": 7.017361, + "weight": 4.75329315798963e-05 + }, + { + "days": 7.018056, + "weight": 2.9503198911659772e-05 + }, + { + "days": 7.051389, + "weight": 3.4420398730269734e-05 + }, + { + "days": 7.965278, + "weight": 3.278133212406641e-05 + }, + { + "days": 9.828472, + "weight": 9.014866334118263e-05 + }, + { + "days": 9.863889, + "weight": 4.75329315798963e-05 + }, + { + "days": 9.931944, + "weight": 9.342679655358928e-05 + }, + { + "days": 9.985417, + "weight": 4.097666515508301e-05 + }, + { + "days": 10.048611, + "weight": 3.7698531942676374e-05 + }, + { + "days": 10.845833, + "weight": 3.605946533647306e-05 + }, + { + "days": 10.914583, + "weight": 3.1142265517863095e-05 + }, + { + "days": 11.076389, + "weight": 6.392359764192951e-05 + }, + { + "days": 11.150694, + "weight": 3.1142265517863095e-05 + }, + { + "days": 11.209722, + "weight": 4.917199818609962e-05 + }, + { + "days": 11.26875, + "weight": 4.425479836748966e-05 + }, + { + "days": 11.844444, + "weight": 3.7698531942676374e-05 + }, + { + "days": 11.904861, + "weight": 3.605946533647306e-05 + }, + { + "days": 12.906944, + "weight": 3.9337598548879697e-05 + }, + { + "days": 16.880556, + "weight": 4.097666515508301e-05 + }, + { + "days": 17.958333, + "weight": 4.917199818609962e-05 + }, + { + "days": 22.287131, + "weight": 0.004166666666666667 + }, + { + "days": 25.1029, + "weight": 0.004166666666666667 + }, + { + "days": 28.274415, + "weight": 0.004166666666666667 + }, + { + "days": 31.846621, + "weight": 0.004166666666666667 + }, + { + "days": 35.870141, + "weight": 0.004166666666666667 + }, + { + "days": 40.401996, + "weight": 0.004166666666666667 + }, + { + "days": 45.506408, + "weight": 0.004166666666666667 + }, + { + "days": 51.255714, + "weight": 0.004166666666666667 + }, + { + "days": 57.73139, + "weight": 0.004166666666666667 + }, + { + "days": 65.025208, + "weight": 0.004166666666666667 + }, + { + "days": 73.240531, + "weight": 0.004166666666666667 + }, + { + "days": 82.493782, + "weight": 0.004166666666666667 + }, + { + "days": 92.916094, + "weight": 0.004166666666666667 + }, + { + "days": 104.655168, + "weight": 0.004166666666666667 + }, + { + "days": 117.877362, + "weight": 0.004166666666666667 + }, + { + "days": 132.770057, + "weight": 0.004166666666666667 + }, + { + "days": 149.544303, + "weight": 0.004166666666666667 + }, + { + "days": 168.437818, + "weight": 0.004166666666666667 + }, + { + "days": 189.71835, + "weight": 0.004166666666666667 + }, + { + "days": 213.687476, + "weight": 0.004166666666666667 + }, + { + "days": 240.684877, + "weight": 0.004166666666666667 + }, + { + "days": 271.093144, + "weight": 0.004166666666666667 + }, + { + "days": 305.34321, + "weight": 0.004166666666666667 + }, + { + "days": 343.920448, + "weight": 0.004166666666666667 + }, + { + "new_client": true, + "weight": 0.019885156066458687 + } + ] +} \ No newline at end of file diff --git a/tools/DeltaIndexTestTool/dist-tail-20.json b/tools/DeltaIndexTestTool/dist-tail-20.json new file mode 100644 index 0000000000..670a029be0 --- /dev/null +++ b/tools/DeltaIndexTestTool/dist-tail-20.json @@ -0,0 +1,7301 @@ +{ + "description": "Telemetry-derived from C:\\Users\\johnmcp\\Downloads\\export.csv: 686,366 download events, 1.77% net-new clients, observed ages 0-18.0 days, plus 20.0% reinstated stale tail out to 344 days", + "buckets": [ + { + "days": 0.0, + "weight": 4.079454664328265e-05 + }, + { + "days": 0.000694, + "weight": 0.00043271358403767666 + }, + { + "days": 0.028472, + "weight": 0.0017425099209059302 + }, + { + "days": 0.042361, + "weight": 4.6622339020894455e-05 + }, + { + "days": 0.043056, + "weight": 6.556266424813284e-05 + }, + { + "days": 0.047222, + "weight": 0.006537326099586045 + }, + { + "days": 0.049306, + "weight": 4.079454664328265e-05 + }, + { + "days": 0.050694, + "weight": 0.00283813488789695 + }, + { + "days": 0.051389, + "weight": 0.0070880524792703604 + }, + { + "days": 0.052083, + "weight": 2.9138961888059035e-05 + }, + { + "days": 0.054167, + "weight": 0.004095481093366698 + }, + { + "days": 0.054861, + "weight": 0.006413485511561794 + }, + { + "days": 0.055556, + "weight": 0.005271238205549879 + }, + { + "days": 0.056944, + "weight": 0.005603422371073753 + }, + { + "days": 0.057639, + "weight": 0.007732023536996465 + }, + { + "days": 0.058333, + "weight": 0.0034311127623189515 + }, + { + "days": 0.059028, + "weight": 0.03539946784970852 + }, + { + "days": 0.059722, + "weight": 0.016157554366928735 + }, + { + "days": 0.060417, + "weight": 0.006782093379445741 + }, + { + "days": 0.061111, + "weight": 0.014544712826424668 + }, + { + "days": 0.061806, + "weight": 0.016266825474008956 + }, + { + "days": 0.0625, + "weight": 0.0030348228806413484 + }, + { + "days": 0.063194, + "weight": 0.011224328119280341 + }, + { + "days": 0.063889, + "weight": 0.01250207159807173 + }, + { + "days": 0.064583, + "weight": 0.002959061579732395 + }, + { + "days": 0.065278, + "weight": 0.006581034542418133 + }, + { + "days": 0.065972, + "weight": 0.00822738588909347 + }, + { + "days": 0.066667, + "weight": 0.013556902018419466 + }, + { + "days": 0.067361, + "weight": 0.003750184394993198 + }, + { + "days": 0.069444, + "weight": 0.00525666872460585 + }, + { + "days": 0.070833, + "weight": 0.003211113600064106 + }, + { + "days": 0.072222, + "weight": 0.0033976029561476836 + }, + { + "days": 0.072917, + "weight": 0.0029809158011484395 + }, + { + "days": 0.074306, + "weight": 0.010382212120715434 + }, + { + "days": 0.076389, + "weight": 5.2450131398506267e-05 + }, + { + "days": 0.078472, + "weight": 0.004206209148541321 + }, + { + "days": 0.084028, + "weight": 0.0027915125488760556 + }, + { + "days": 0.084722, + "weight": 0.004050315702440206 + }, + { + "days": 0.085417, + "weight": 0.004124620055254756 + }, + { + "days": 0.0875, + "weight": 0.00019231714846118964 + }, + { + "days": 0.088889, + "weight": 0.0019406548617447318 + }, + { + "days": 0.102083, + "weight": 0.0010431748355925134 + }, + { + "days": 0.109722, + "weight": 0.000999466392760425 + }, + { + "days": 0.110417, + "weight": 0.0016099276443152617 + }, + { + "days": 0.113194, + "weight": 0.004019719792457744 + }, + { + "days": 0.114583, + "weight": 0.0012908560116410154 + }, + { + "days": 0.115278, + "weight": 5.827792377611807e-05 + }, + { + "days": 0.115972, + "weight": 3.350980617126789e-05 + }, + { + "days": 0.116667, + "weight": 0.0013083393887738507 + }, + { + "days": 0.117361, + "weight": 0.002609394037075687 + }, + { + "days": 0.118056, + "weight": 0.0026836983898902373 + }, + { + "days": 0.11875, + "weight": 0.0017279404399619008 + }, + { + "days": 0.119444, + "weight": 0.001360789520172357 + }, + { + "days": 0.120139, + "weight": 0.0017556224537555569 + }, + { + "days": 0.120833, + "weight": 0.0015312524472175023 + }, + { + "days": 0.121528, + "weight": 0.0015531066686335466 + }, + { + "days": 0.122222, + "weight": 0.0014132396515708632 + }, + { + "days": 0.122917, + "weight": 0.0011305917212566907 + }, + { + "days": 0.123611, + "weight": 0.0005419846911178981 + }, + { + "days": 0.124306, + "weight": 0.001602642903843247 + }, + { + "days": 0.125, + "weight": 0.0005958917706108072 + }, + { + "days": 0.125694, + "weight": 0.0037312440697659594 + }, + { + "days": 0.126389, + "weight": 0.0006876795005581933 + }, + { + "days": 0.127083, + "weight": 0.0022378722730029338 + }, + { + "days": 0.127778, + "weight": 0.0022553556501357696 + }, + { + "days": 0.130556, + "weight": 0.0002476811760485018 + }, + { + "days": 0.13125, + "weight": 0.0011728432159943763 + }, + { + "days": 0.131944, + "weight": 0.0006264876805932692 + }, + { + "days": 0.132639, + "weight": 0.001210723866448853 + }, + { + "days": 0.133333, + "weight": 0.0024083352000480794 + }, + { + "days": 0.134028, + "weight": 0.00187072135321339 + }, + { + "days": 0.134722, + "weight": 0.0005769514453835689 + }, + { + "days": 0.136111, + "weight": 0.0011903265931272116 + }, + { + "days": 0.1375, + "weight": 0.0004968193001914066 + }, + { + "days": 0.138194, + "weight": 0.0004385413764152885 + }, + { + "days": 0.14375, + "weight": 0.0003482105945623055 + }, + { + "days": 0.144444, + "weight": 0.001027148406554081 + }, + { + "days": 0.147917, + "weight": 0.0009441023651731127 + }, + { + "days": 0.148611, + "weight": 0.00021271442178283096 + }, + { + "days": 0.149306, + "weight": 0.0007998645038272206 + }, + { + "days": 0.150694, + "weight": 0.0010941680188966169 + }, + { + "days": 0.161111, + "weight": 0.0006177459920268516 + }, + { + "days": 0.164583, + "weight": 3.059590998246199e-05 + }, + { + "days": 0.168056, + "weight": 0.0006090043034604339 + }, + { + "days": 0.169444, + "weight": 0.0013680742606443717 + }, + { + "days": 0.172222, + "weight": 0.0006090043034604339 + }, + { + "days": 0.172917, + "weight": 5.3907079492909214e-05 + }, + { + "days": 0.173611, + "weight": 0.000904764766624233 + }, + { + "days": 0.175, + "weight": 0.0004603955978313328 + }, + { + "days": 0.175694, + "weight": 0.00112039308459587 + }, + { + "days": 0.176389, + "weight": 0.0005201304697018537 + }, + { + "days": 0.177083, + "weight": 0.0008945661299634124 + }, + { + "days": 0.177778, + "weight": 0.0016492652428641414 + }, + { + "days": 0.178472, + "weight": 0.0005376138468346892 + }, + { + "days": 0.179167, + "weight": 0.0019523104464999554 + }, + { + "days": 0.179861, + "weight": 0.0008989369742466212 + }, + { + "days": 0.18125, + "weight": 0.0021271442178283097 + }, + { + "days": 0.181944, + "weight": 0.0011568167869559437 + }, + { + "days": 0.182639, + "weight": 0.0004676803383033475 + }, + { + "days": 0.183333, + "weight": 0.00046622339020894457 + }, + { + "days": 0.184028, + "weight": 0.0008843674933025918 + }, + { + "days": 0.184722, + "weight": 0.0012019821778824352 + }, + { + "days": 0.185417, + "weight": 0.0006148320958380457 + }, + { + "days": 0.186111, + "weight": 0.002207276363020472 + }, + { + "days": 0.186806, + "weight": 0.00026516455318133724 + }, + { + "days": 0.1875, + "weight": 0.00038026345263917044 + }, + { + "days": 0.188889, + "weight": 0.0006993350853134169 + }, + { + "days": 0.190972, + "weight": 0.0004283427397544678 + }, + { + "days": 0.192361, + "weight": 0.0012500614649977326 + }, + { + "days": 0.194444, + "weight": 0.0005652958606283453 + }, + { + "days": 0.195139, + "weight": 0.0024505866947857648 + }, + { + "days": 0.196528, + "weight": 0.0010111219775156485 + }, + { + "days": 0.197222, + "weight": 0.00035403838693991727 + }, + { + "days": 0.197917, + "weight": 0.0002301977989156664 + }, + { + "days": 0.2, + "weight": 0.0003161577364854405 + }, + { + "days": 0.202083, + "weight": 0.0012893990635466123 + }, + { + "days": 0.204167, + "weight": 0.0005405277430234951 + }, + { + "days": 0.208333, + "weight": 0.0005798653415723748 + }, + { + "days": 0.210417, + "weight": 0.0007066198257854316 + }, + { + "days": 0.211111, + "weight": 0.00043271358403767666 + }, + { + "days": 0.2125, + "weight": 0.0005259582620794656 + }, + { + "days": 0.218056, + "weight": 0.00018066156370596603 + }, + { + "days": 0.222917, + "weight": 0.00029138961888059035 + }, + { + "days": 0.227083, + "weight": 0.0002797340341253667 + }, + { + "days": 0.229167, + "weight": 9.470162613619187e-05 + }, + { + "days": 0.23125, + "weight": 0.0006410571615372988 + }, + { + "days": 0.232639, + "weight": 0.00010635721089141548 + }, + { + "days": 0.233333, + "weight": 8.74168856641771e-05 + }, + { + "days": 0.234722, + "weight": 9.615857423059482e-05 + }, + { + "days": 0.235417, + "weight": 9.324467804178891e-05 + }, + { + "days": 0.236111, + "weight": 0.00024622422795409887 + }, + { + "days": 0.2375, + "weight": 0.00024331033176529295 + }, + { + "days": 0.238194, + "weight": 0.00010052941851380368 + }, + { + "days": 0.238889, + "weight": 0.00038026345263917044 + }, + { + "days": 0.239583, + "weight": 0.0002025157851220103 + }, + { + "days": 0.240278, + "weight": 0.0003715217640727527 + }, + { + "days": 0.240972, + "weight": 0.0003948329335831999 + }, + { + "days": 0.242361, + "weight": 0.00032489942505185826 + }, + { + "days": 0.243056, + "weight": 0.0007211893067294612 + }, + { + "days": 0.24375, + "weight": 0.0004968193001914066 + }, + { + "days": 0.244444, + "weight": 0.00010198636660820662 + }, + { + "days": 0.245833, + "weight": 0.00021562831797163686 + }, + { + "days": 0.247917, + "weight": 0.000272449293653352 + }, + { + "days": 0.248611, + "weight": 7.284740472014759e-05 + }, + { + "days": 0.249306, + "weight": 0.00022291305844365162 + }, + { + "days": 0.251389, + "weight": 0.00011364195136343023 + }, + { + "days": 0.252083, + "weight": 0.0010548304203477372 + }, + { + "days": 0.252778, + "weight": 0.00024622422795409887 + }, + { + "days": 0.253472, + "weight": 0.0004166871549992442 + }, + { + "days": 0.254167, + "weight": 0.00020980052559402507 + }, + { + "days": 0.254861, + "weight": 9.907247041940072e-05 + }, + { + "days": 0.255556, + "weight": 0.00011946974374104205 + }, + { + "days": 0.25625, + "weight": 0.00010781415898581843 + }, + { + "days": 0.258333, + "weight": 0.00023311169510447228 + }, + { + "days": 0.259028, + "weight": 7.867519709775939e-05 + }, + { + "days": 0.259722, + "weight": 6.993350853134168e-05 + }, + { + "days": 0.261111, + "weight": 0.00022582695463245752 + }, + { + "days": 0.265278, + "weight": 0.00013403922468507157 + }, + { + "days": 0.268056, + "weight": 0.0001777476675171601 + }, + { + "days": 0.271528, + "weight": 0.0003992037778664088 + }, + { + "days": 0.274306, + "weight": 9.470162613619187e-05 + }, + { + "days": 0.275, + "weight": 0.00010198636660820662 + }, + { + "days": 0.276389, + "weight": 0.0001150988994578332 + }, + { + "days": 0.279861, + "weight": 0.0002025157851220103 + }, + { + "days": 0.281944, + "weight": 0.00014278091325148928 + }, + { + "days": 0.284028, + "weight": 3.788065045447675e-05 + }, + { + "days": 0.286806, + "weight": 0.00011655584755223614 + }, + { + "days": 0.288194, + "weight": 7.867519709775939e-05 + }, + { + "days": 0.290278, + "weight": 0.00021271442178283096 + }, + { + "days": 0.292361, + "weight": 9.761552232499777e-05 + }, + { + "days": 0.293056, + "weight": 0.0001267544842130568 + }, + { + "days": 0.295139, + "weight": 0.00011655584755223614 + }, + { + "days": 0.297222, + "weight": 6.993350853134168e-05 + }, + { + "days": 0.297917, + "weight": 0.0003511244907511114 + }, + { + "days": 0.299306, + "weight": 0.00039628988167760286 + }, + { + "days": 0.3, + "weight": 0.00021562831797163686 + }, + { + "days": 0.300694, + "weight": 0.00029721741125820215 + }, + { + "days": 0.301389, + "weight": 0.00018357545989477193 + }, + { + "days": 0.303472, + "weight": 6.556266424813284e-05 + }, + { + "days": 0.304167, + "weight": 0.00030741604791902283 + }, + { + "days": 0.305556, + "weight": 0.0003161577364854405 + }, + { + "days": 0.30625, + "weight": 0.00017629071942275716 + }, + { + "days": 0.306944, + "weight": 0.00013986701706268336 + }, + { + "days": 0.308333, + "weight": 0.00024185338367089 + }, + { + "days": 0.309722, + "weight": 0.0010184067179876634 + }, + { + "days": 0.310417, + "weight": 0.0001150988994578332 + }, + { + "days": 0.311111, + "weight": 0.00016172123847872766 + }, + { + "days": 0.311806, + "weight": 0.0003875481931111852 + }, + { + "days": 0.3125, + "weight": 0.0002447672798596959 + }, + { + "days": 0.313194, + "weight": 0.00035695228312872316 + }, + { + "days": 0.313889, + "weight": 0.00011072805517462434 + }, + { + "days": 0.314583, + "weight": 0.00015006565372350402 + }, + { + "days": 0.315972, + "weight": 0.00011218500326902729 + }, + { + "days": 0.316667, + "weight": 8.74168856641771e-05 + }, + { + "days": 0.317361, + "weight": 0.00010781415898581843 + }, + { + "days": 0.31875, + "weight": 4.22514947376856e-05 + }, + { + "days": 0.320139, + "weight": 0.00030013130744700804 + }, + { + "days": 0.320833, + "weight": 8.013214519216235e-05 + }, + { + "days": 0.322222, + "weight": 0.00016463513466753355 + }, + { + "days": 0.323611, + "weight": 4.370844283208855e-05 + }, + { + "days": 0.326389, + "weight": 0.000151522601817907 + }, + { + "days": 0.327778, + "weight": 0.00012238363992984796 + }, + { + "days": 0.33125, + "weight": 0.00017920461561156306 + }, + { + "days": 0.332639, + "weight": 0.006036135955111429 + }, + { + "days": 0.335417, + "weight": 0.00016754903085633945 + }, + { + "days": 0.338194, + "weight": 8.45029894753712e-05 + }, + { + "days": 0.340278, + "weight": 0.0001384100689682804 + }, + { + "days": 0.340972, + "weight": 0.00030158825554141104 + }, + { + "days": 0.343056, + "weight": 0.00011655584755223614 + }, + { + "days": 0.34375, + "weight": 0.00014715175753469813 + }, + { + "days": 0.345139, + "weight": 2.7682013793656084e-05 + }, + { + "days": 0.345833, + "weight": 0.00013986701706268336 + }, + { + "days": 0.349306, + "weight": 9.470162613619187e-05 + }, + { + "days": 0.353472, + "weight": 0.00011946974374104205 + }, + { + "days": 0.354861, + "weight": 0.00010927110708022139 + }, + { + "days": 0.356944, + "weight": 6.847656043693873e-05 + }, + { + "days": 0.357639, + "weight": 0.00013695312087387746 + }, + { + "days": 0.358333, + "weight": 5.9734871870521025e-05 + }, + { + "days": 0.359028, + "weight": 0.0005361568987402863 + }, + { + "days": 0.359722, + "weight": 0.00021999916225484573 + }, + { + "days": 0.360417, + "weight": 0.0001748337713283542 + }, + { + "days": 0.361111, + "weight": 0.00010198636660820662 + }, + { + "days": 0.361806, + "weight": 4.370844283208855e-05 + }, + { + "days": 0.3625, + "weight": 0.00027390624174775493 + }, + { + "days": 0.363194, + "weight": 0.0002054296813108162 + }, + { + "days": 0.363889, + "weight": 8.15890932865653e-05 + }, + { + "days": 0.365972, + "weight": 0.00017191987513954832 + }, + { + "days": 0.366667, + "weight": 8.304604138096825e-05 + }, + { + "days": 0.367361, + "weight": 6.119181996492398e-05 + }, + { + "days": 0.36875, + "weight": 0.00010198636660820662 + }, + { + "days": 0.369444, + "weight": 0.0001267544842130568 + }, + { + "days": 0.370139, + "weight": 0.00024185338367089 + }, + { + "days": 0.371528, + "weight": 0.0003788065045447675 + }, + { + "days": 0.372222, + "weight": 0.00014423786134589223 + }, + { + "days": 0.372917, + "weight": 0.00020105883702760735 + }, + { + "days": 0.373611, + "weight": 0.00017920461561156306 + }, + { + "days": 0.375, + "weight": 0.0014656897829693695 + }, + { + "days": 0.376389, + "weight": 0.0003452966983734996 + }, + { + "days": 0.377083, + "weight": 0.0001384100689682804 + }, + { + "days": 0.377778, + "weight": 0.0001631781865731306 + }, + { + "days": 0.378472, + "weight": 0.00014423786134589223 + }, + { + "days": 0.379861, + "weight": 0.00011655584755223614 + }, + { + "days": 0.38125, + "weight": 0.00012238363992984796 + }, + { + "days": 0.3875, + "weight": 0.0001150988994578332 + }, + { + "days": 0.388889, + "weight": 4.516539092649151e-05 + }, + { + "days": 0.390278, + "weight": 0.00010344331470260957 + }, + { + "days": 0.392361, + "weight": 0.00010344331470260957 + }, + { + "days": 0.397222, + "weight": 0.00018794630417798077 + }, + { + "days": 0.398611, + "weight": 0.00021271442178283096 + }, + { + "days": 0.399306, + "weight": 0.00011655584755223614 + }, + { + "days": 0.400694, + "weight": 0.0001908602003667867 + }, + { + "days": 0.404167, + "weight": 0.00011801279564663909 + }, + { + "days": 0.406944, + "weight": 0.001660920827619365 + }, + { + "days": 0.407639, + "weight": 0.00014569480944029518 + }, + { + "days": 0.409028, + "weight": 0.00011218500326902729 + }, + { + "days": 0.4125, + "weight": 0.00023602559129327818 + }, + { + "days": 0.414583, + "weight": 6.410571615372988e-05 + }, + { + "days": 0.415972, + "weight": 7.867519709775939e-05 + }, + { + "days": 0.418056, + "weight": 0.00024331033176529295 + }, + { + "days": 0.41875, + "weight": 9.615857423059482e-05 + }, + { + "days": 0.419444, + "weight": 0.0001908602003667867 + }, + { + "days": 0.420139, + "weight": 0.0002768201379365608 + }, + { + "days": 0.421528, + "weight": 0.0003788065045447675 + }, + { + "days": 0.422222, + "weight": 0.00018940325227238374 + }, + { + "days": 0.422917, + "weight": 7.721824900335645e-05 + }, + { + "days": 0.424306, + "weight": 0.00019523104464999554 + }, + { + "days": 0.425, + "weight": 0.0002753631898421579 + }, + { + "days": 0.426389, + "weight": 0.00010927110708022139 + }, + { + "days": 0.427083, + "weight": 0.0001908602003667867 + }, + { + "days": 0.427778, + "weight": 0.00024331033176529295 + }, + { + "days": 0.429861, + "weight": 0.0014074118591932514 + }, + { + "days": 0.43125, + "weight": 4.370844283208855e-05 + }, + { + "days": 0.431944, + "weight": 0.00015880734228992173 + }, + { + "days": 0.432639, + "weight": 0.0001937740965555926 + }, + { + "days": 0.434028, + "weight": 0.00017046292704514537 + }, + { + "days": 0.435417, + "weight": 0.00011801279564663909 + }, + { + "days": 0.436806, + "weight": 3.9337598548879697e-05 + }, + { + "days": 0.4375, + "weight": 0.0006614544348589401 + }, + { + "days": 0.438194, + "weight": 0.00019668799274439848 + }, + { + "days": 0.438889, + "weight": 0.00024185338367089 + }, + { + "days": 0.440278, + "weight": 0.00013112532849626567 + }, + { + "days": 0.442361, + "weight": 0.00015006565372350402 + }, + { + "days": 0.443056, + "weight": 0.0001296683804018627 + }, + { + "days": 0.446528, + "weight": 0.00010490026279701253 + }, + { + "days": 0.447222, + "weight": 0.0002986743593526051 + }, + { + "days": 0.447917, + "weight": 0.00018211851180036898 + }, + { + "days": 0.449306, + "weight": 0.00013695312087387746 + }, + { + "days": 0.453472, + "weight": 0.000332184165523873 + }, + { + "days": 0.457639, + "weight": 0.0003205285807686494 + }, + { + "days": 0.458333, + "weight": 0.0003875481931111852 + }, + { + "days": 0.459028, + "weight": 6.993350853134168e-05 + }, + { + "days": 0.460417, + "weight": 0.00019523104464999554 + }, + { + "days": 0.4625, + "weight": 0.0008683410642641592 + }, + { + "days": 0.466667, + "weight": 0.0017643641423219746 + }, + { + "days": 0.468056, + "weight": 0.0003817204007335734 + }, + { + "days": 0.470833, + "weight": 4.079454664328265e-05 + }, + { + "days": 0.472222, + "weight": 0.00017191987513954832 + }, + { + "days": 0.472917, + "weight": 0.00015589344610111584 + }, + { + "days": 0.473611, + "weight": 0.0004283427397544678 + }, + { + "days": 0.474306, + "weight": 0.0004647664421145416 + }, + { + "days": 0.475, + "weight": 0.0002535089684261136 + }, + { + "days": 0.477083, + "weight": 0.0008421159985649062 + }, + { + "days": 0.477778, + "weight": 0.00028264793031417267 + }, + { + "days": 0.479167, + "weight": 0.00040066072596081176 + }, + { + "days": 0.480556, + "weight": 0.0004385413764152885 + }, + { + "days": 0.48125, + "weight": 0.00048224981924737705 + }, + { + "days": 0.483333, + "weight": 0.0003773495564503645 + }, + { + "days": 0.484028, + "weight": 0.0011699293198055704 + }, + { + "days": 0.484722, + "weight": 0.00026225065699253135 + }, + { + "days": 0.485417, + "weight": 0.0002535089684261136 + }, + { + "days": 0.486806, + "weight": 0.00105628736844214 + }, + { + "days": 0.4875, + "weight": 0.00047496507877536225 + }, + { + "days": 0.488194, + "weight": 0.00030158825554141104 + }, + { + "days": 0.488889, + "weight": 0.00026516455318133724 + }, + { + "days": 0.490278, + "weight": 0.0006935072929358051 + }, + { + "days": 0.490972, + "weight": 0.0002025157851220103 + }, + { + "days": 0.491667, + "weight": 0.00035403838693991727 + }, + { + "days": 0.492361, + "weight": 0.00022582695463245752 + }, + { + "days": 0.493056, + "weight": 0.0006935072929358051 + }, + { + "days": 0.49375, + "weight": 0.0005609250163451364 + }, + { + "days": 0.494444, + "weight": 0.00307998827156784 + }, + { + "days": 0.495833, + "weight": 0.00033946890599588774 + }, + { + "days": 0.498611, + "weight": 0.0006556266424813283 + }, + { + "days": 0.499306, + "weight": 0.00023165474701006934 + }, + { + "days": 0.500694, + "weight": 0.00025787981270932245 + }, + { + "days": 0.502083, + "weight": 0.00028264793031417267 + }, + { + "days": 0.50625, + "weight": 0.00032489942505185826 + }, + { + "days": 0.506944, + "weight": 0.000665825279142149 + }, + { + "days": 0.509028, + "weight": 0.00032489942505185826 + }, + { + "days": 0.511111, + "weight": 0.0009703274308723659 + }, + { + "days": 0.5125, + "weight": 0.00012238363992984796 + }, + { + "days": 0.517361, + "weight": 0.00039628988167760286 + }, + { + "days": 0.51875, + "weight": 0.00023311169510447228 + }, + { + "days": 0.521528, + "weight": 0.0004807928711529741 + }, + { + "days": 0.522917, + "weight": 0.00023748253938768113 + }, + { + "days": 0.524306, + "weight": 0.00034383975027909664 + }, + { + "days": 0.525, + "weight": 0.0001602642903843247 + }, + { + "days": 0.526389, + "weight": 0.00022291305844365162 + }, + { + "days": 0.527083, + "weight": 0.00016172123847872766 + }, + { + "days": 0.532639, + "weight": 0.0003584092312231261 + }, + { + "days": 0.533333, + "weight": 0.0006439710577261047 + }, + { + "days": 0.535417, + "weight": 0.00039628988167760286 + }, + { + "days": 0.536111, + "weight": 0.00027099234555894903 + }, + { + "days": 0.536806, + "weight": 0.0011990682816936293 + }, + { + "days": 0.538194, + "weight": 7.57613009089535e-05 + }, + { + "days": 0.539583, + "weight": 0.0005041040406634213 + }, + { + "days": 0.540278, + "weight": 0.0004807928711529741 + }, + { + "days": 0.543056, + "weight": 0.0004079454664328265 + }, + { + "days": 0.544444, + "weight": 0.000754699112900729 + }, + { + "days": 0.545833, + "weight": 0.0003263563731462612 + }, + { + "days": 0.546528, + "weight": 0.0006279446286876722 + }, + { + "days": 0.547222, + "weight": 0.0002841048784085756 + }, + { + "days": 0.548611, + "weight": 0.00017920461561156306 + }, + { + "days": 0.549306, + "weight": 0.00026807844937014314 + }, + { + "days": 0.55, + "weight": 0.0012762865306969857 + }, + { + "days": 0.550694, + "weight": 0.000847943790942518 + }, + { + "days": 0.551389, + "weight": 0.00036715091978954385 + }, + { + "days": 0.552083, + "weight": 8.595993756977416e-05 + }, + { + "days": 0.552778, + "weight": 0.00017337682323395127 + }, + { + "days": 0.553472, + "weight": 0.0004501969611705121 + }, + { + "days": 0.554167, + "weight": 5.3907079492909214e-05 + }, + { + "days": 0.554861, + "weight": 0.00023165474701006934 + }, + { + "days": 0.555556, + "weight": 0.0002054296813108162 + }, + { + "days": 0.556944, + "weight": 0.0001544364980067129 + }, + { + "days": 0.557639, + "weight": 0.0002593367608037254 + }, + { + "days": 0.558333, + "weight": 0.0002870187745973815 + }, + { + "days": 0.559722, + "weight": 0.0003525814388455143 + }, + { + "days": 0.560417, + "weight": 9.178772994738596e-05 + }, + { + "days": 0.568056, + "weight": 0.000120926691835445 + }, + { + "days": 0.570139, + "weight": 0.00046913728639775046 + }, + { + "days": 0.570833, + "weight": 7.57613009089535e-05 + }, + { + "days": 0.572917, + "weight": 0.00013112532849626567 + }, + { + "days": 0.575, + "weight": 0.00014715175753469813 + }, + { + "days": 0.58125, + "weight": 0.00020688662940521914 + }, + { + "days": 0.584722, + "weight": 0.0005580111201563305 + }, + { + "days": 0.5875, + "weight": 3.205285807686494e-05 + }, + { + "days": 0.590278, + "weight": 6.410571615372988e-05 + }, + { + "days": 0.591667, + "weight": 0.0003234424769574553 + }, + { + "days": 0.592361, + "weight": 0.00018503240798917487 + }, + { + "days": 0.593056, + "weight": 7.430435281455054e-05 + }, + { + "days": 0.59375, + "weight": 9.615857423059482e-05 + }, + { + "days": 0.595833, + "weight": 0.00010344331470260957 + }, + { + "days": 0.597222, + "weight": 0.00021854221416044278 + }, + { + "days": 0.597917, + "weight": 0.0009688704827779629 + }, + { + "days": 0.598611, + "weight": 0.0003423828021846937 + }, + { + "days": 0.599306, + "weight": 7.721824900335645e-05 + }, + { + "days": 0.6, + "weight": 0.00011072805517462434 + }, + { + "days": 0.601389, + "weight": 0.00024185338367089 + }, + { + "days": 0.602083, + "weight": 4.953623520970036e-05 + }, + { + "days": 0.602778, + "weight": 0.0001748337713283542 + }, + { + "days": 0.603472, + "weight": 0.00018211851180036898 + }, + { + "days": 0.605556, + "weight": 0.0003875481931111852 + }, + { + "days": 0.606944, + "weight": 0.00014569480944029518 + }, + { + "days": 0.607639, + "weight": 0.0001267544842130568 + }, + { + "days": 0.609028, + "weight": 0.00013258227659066862 + }, + { + "days": 0.609722, + "weight": 0.0002083435774996221 + }, + { + "days": 0.610417, + "weight": 0.00011072805517462434 + }, + { + "days": 0.611111, + "weight": 0.00151522601817907 + }, + { + "days": 0.611806, + "weight": 0.00026516455318133724 + }, + { + "days": 0.6125, + "weight": 5.6820975681715117e-05 + }, + { + "days": 0.613194, + "weight": 4.370844283208855e-05 + }, + { + "days": 0.613889, + "weight": 0.00025205202033171066 + }, + { + "days": 0.614583, + "weight": 0.00022874085082126344 + }, + { + "days": 0.615278, + "weight": 6.410571615372988e-05 + }, + { + "days": 0.615972, + "weight": 6.410571615372988e-05 + }, + { + "days": 0.616667, + "weight": 0.0002986743593526051 + }, + { + "days": 0.617361, + "weight": 0.00018357545989477193 + }, + { + "days": 0.61875, + "weight": 0.00015880734228992173 + }, + { + "days": 0.619444, + "weight": 7.284740472014759e-05 + }, + { + "days": 0.620139, + "weight": 0.0002637076050869343 + }, + { + "days": 0.622222, + "weight": 6.847656043693873e-05 + }, + { + "days": 0.623611, + "weight": 8.304604138096825e-05 + }, + { + "days": 0.624306, + "weight": 9.615857423059482e-05 + }, + { + "days": 0.628472, + "weight": 6.847656043693873e-05 + }, + { + "days": 0.631944, + "weight": 0.00021854221416044278 + }, + { + "days": 0.632639, + "weight": 6.993350853134168e-05 + }, + { + "days": 0.634028, + "weight": 0.00036423702360073796 + }, + { + "days": 0.642361, + "weight": 9.178772994738596e-05 + }, + { + "days": 0.644444, + "weight": 0.00044291222069849735 + }, + { + "days": 0.648611, + "weight": 9.470162613619187e-05 + }, + { + "days": 0.652778, + "weight": 0.0013374783506619097 + }, + { + "days": 0.654167, + "weight": 0.0003744356602615586 + }, + { + "days": 0.654861, + "weight": 0.00011364195136343023 + }, + { + "days": 0.655556, + "weight": 0.0003234424769574553 + }, + { + "days": 0.65625, + "weight": 4.807928711529741e-05 + }, + { + "days": 0.656944, + "weight": 6.556266424813284e-05 + }, + { + "days": 0.657639, + "weight": 4.22514947376856e-05 + }, + { + "days": 0.658333, + "weight": 0.00017337682323395127 + }, + { + "days": 0.659028, + "weight": 5.9734871870521025e-05 + }, + { + "days": 0.660417, + "weight": 0.00019231714846118964 + }, + { + "days": 0.661806, + "weight": 0.00014423786134589223 + }, + { + "days": 0.6625, + "weight": 6.119181996492398e-05 + }, + { + "days": 0.663194, + "weight": 8.887383375858005e-05 + }, + { + "days": 0.663889, + "weight": 4.807928711529741e-05 + }, + { + "days": 0.664583, + "weight": 0.0007139045662574463 + }, + { + "days": 0.665278, + "weight": 0.00010344331470260957 + }, + { + "days": 0.665972, + "weight": 0.00014423786134589223 + }, + { + "days": 0.666667, + "weight": 9.324467804178891e-05 + }, + { + "days": 0.667361, + "weight": 0.0001267544842130568 + }, + { + "days": 0.668056, + "weight": 3.350980617126789e-05 + }, + { + "days": 0.66875, + "weight": 0.0008377451542816973 + }, + { + "days": 0.669444, + "weight": 0.00017337682323395127 + }, + { + "days": 0.670139, + "weight": 0.001354961727794745 + }, + { + "days": 0.670833, + "weight": 0.0001267544842130568 + }, + { + "days": 0.671528, + "weight": 0.00011364195136343023 + }, + { + "days": 0.672917, + "weight": 0.00019814494083880143 + }, + { + "days": 0.673611, + "weight": 0.00017191987513954832 + }, + { + "days": 0.674306, + "weight": 0.00013549617277947452 + }, + { + "days": 0.675, + "weight": 6.264876805932693e-05 + }, + { + "days": 0.675694, + "weight": 0.00020105883702760735 + }, + { + "days": 0.676389, + "weight": 0.00015589344610111584 + }, + { + "days": 0.677083, + "weight": 0.00018648935608357782 + }, + { + "days": 0.678472, + "weight": 8.013214519216235e-05 + }, + { + "days": 0.68125, + "weight": 4.807928711529741e-05 + }, + { + "days": 0.681944, + "weight": 0.00022582695463245752 + }, + { + "days": 0.682639, + "weight": 0.00010198636660820662 + }, + { + "days": 0.683333, + "weight": 0.0003132438402966346 + }, + { + "days": 0.684028, + "weight": 0.00036132312741193206 + }, + { + "days": 0.6875, + "weight": 0.00010781415898581843 + }, + { + "days": 0.688889, + "weight": 0.00014423786134589223 + }, + { + "days": 0.690278, + "weight": 4.516539092649151e-05 + }, + { + "days": 0.69375, + "weight": 8.304604138096825e-05 + }, + { + "days": 0.695833, + "weight": 5.536402758731217e-05 + }, + { + "days": 0.698611, + "weight": 0.0007241032029182671 + }, + { + "days": 0.70625, + "weight": 2.6225065699253133e-05 + }, + { + "days": 0.707639, + "weight": 9.178772994738596e-05 + }, + { + "days": 0.713889, + "weight": 0.0003496675426567084 + }, + { + "days": 0.716667, + "weight": 0.0001296683804018627 + }, + { + "days": 0.71875, + "weight": 0.0006774808638973725 + }, + { + "days": 0.719444, + "weight": 0.0006235737844044634 + }, + { + "days": 0.720139, + "weight": 0.0012296641916760913 + }, + { + "days": 0.720833, + "weight": 0.0003132438402966346 + }, + { + "days": 0.721528, + "weight": 0.00016463513466753355 + }, + { + "days": 0.722222, + "weight": 7.867519709775939e-05 + }, + { + "days": 0.723611, + "weight": 0.00017191987513954832 + }, + { + "days": 0.725, + "weight": 7.284740472014759e-05 + }, + { + "days": 0.725694, + "weight": 9.761552232499777e-05 + }, + { + "days": 0.726389, + "weight": 0.000303045203635814 + }, + { + "days": 0.727083, + "weight": 9.178772994738596e-05 + }, + { + "days": 0.727778, + "weight": 0.00012529753611865386 + }, + { + "days": 0.728472, + "weight": 0.00019231714846118964 + }, + { + "days": 0.729167, + "weight": 0.00024913812414290476 + }, + { + "days": 0.729861, + "weight": 9.033078185298302e-05 + }, + { + "days": 0.730556, + "weight": 0.0010999958112742287 + }, + { + "days": 0.73125, + "weight": 8.013214519216235e-05 + }, + { + "days": 0.731944, + "weight": 6.119181996492398e-05 + }, + { + "days": 0.732639, + "weight": 0.00011218500326902729 + }, + { + "days": 0.733333, + "weight": 6.556266424813284e-05 + }, + { + "days": 0.734028, + "weight": 0.00020980052559402507 + }, + { + "days": 0.734722, + "weight": 7.430435281455054e-05 + }, + { + "days": 0.735417, + "weight": 0.00028556182650297856 + }, + { + "days": 0.736806, + "weight": 8.45029894753712e-05 + }, + { + "days": 0.7375, + "weight": 0.00012238363992984796 + }, + { + "days": 0.738194, + "weight": 0.0005725806011003601 + }, + { + "days": 0.738889, + "weight": 0.00022145611034924868 + }, + { + "days": 0.740278, + "weight": 0.00011801279564663909 + }, + { + "days": 0.740972, + "weight": 5.099318330410331e-05 + }, + { + "days": 0.741667, + "weight": 9.033078185298302e-05 + }, + { + "days": 0.743056, + "weight": 3.496675426567084e-05 + }, + { + "days": 0.744444, + "weight": 8.595993756977416e-05 + }, + { + "days": 0.746528, + "weight": 8.304604138096825e-05 + }, + { + "days": 0.747222, + "weight": 0.0004501969611705121 + }, + { + "days": 0.748611, + "weight": 0.00010927110708022139 + }, + { + "days": 0.749306, + "weight": 0.000120926691835445 + }, + { + "days": 0.75, + "weight": 5.099318330410331e-05 + }, + { + "days": 0.750694, + "weight": 9.761552232499777e-05 + }, + { + "days": 0.7625, + "weight": 5.827792377611807e-05 + }, + { + "days": 0.776389, + "weight": 0.0001908602003667867 + }, + { + "days": 0.777083, + "weight": 5.2450131398506267e-05 + }, + { + "days": 0.777778, + "weight": 4.807928711529741e-05 + }, + { + "days": 0.778472, + "weight": 0.000423971895471259 + }, + { + "days": 0.779167, + "weight": 0.00016172123847872766 + }, + { + "days": 0.779861, + "weight": 0.002017873110748088 + }, + { + "days": 0.780556, + "weight": 0.00037589260835596154 + }, + { + "days": 0.78125, + "weight": 9.178772994738596e-05 + }, + { + "days": 0.781944, + "weight": 8.15890932865653e-05 + }, + { + "days": 0.782639, + "weight": 0.00027827708603096377 + }, + { + "days": 0.783333, + "weight": 7.867519709775939e-05 + }, + { + "days": 0.784028, + "weight": 0.00017191987513954832 + }, + { + "days": 0.785417, + "weight": 0.0003788065045447675 + }, + { + "days": 0.786806, + "weight": 0.0002535089684261136 + }, + { + "days": 0.7875, + "weight": 0.0023194613662894993 + }, + { + "days": 0.788194, + "weight": 0.0004939054040026007 + }, + { + "days": 0.788889, + "weight": 3.788065045447675e-05 + }, + { + "days": 0.789583, + "weight": 0.0006541696943869254 + }, + { + "days": 0.790972, + "weight": 6.119181996492398e-05 + }, + { + "days": 0.791667, + "weight": 0.00029138961888059035 + }, + { + "days": 0.792361, + "weight": 0.0002564228646149195 + }, + { + "days": 0.793056, + "weight": 8.15890932865653e-05 + }, + { + "days": 0.79375, + "weight": 0.00011364195136343023 + }, + { + "days": 0.795833, + "weight": 0.0003656939716951409 + }, + { + "days": 0.796528, + "weight": 0.0001296683804018627 + }, + { + "days": 0.797917, + "weight": 0.00015297954991230994 + }, + { + "days": 0.798611, + "weight": 0.0003161577364854405 + }, + { + "days": 0.799306, + "weight": 8.74168856641771e-05 + }, + { + "days": 0.8, + "weight": 0.0001631781865731306 + }, + { + "days": 0.800694, + "weight": 5.536402758731217e-05 + }, + { + "days": 0.801389, + "weight": 0.0003292702693350671 + }, + { + "days": 0.802778, + "weight": 7.721824900335645e-05 + }, + { + "days": 0.804167, + "weight": 9.761552232499777e-05 + }, + { + "days": 0.804861, + "weight": 0.0002986743593526051 + }, + { + "days": 0.805556, + "weight": 7.284740472014759e-05 + }, + { + "days": 0.80625, + "weight": 5.2450131398506267e-05 + }, + { + "days": 0.808333, + "weight": 8.15890932865653e-05 + }, + { + "days": 0.809028, + "weight": 8.304604138096825e-05 + }, + { + "days": 0.810417, + "weight": 0.0003161577364854405 + }, + { + "days": 0.811111, + "weight": 0.00024185338367089 + }, + { + "days": 0.827083, + "weight": 8.887383375858005e-05 + }, + { + "days": 0.832639, + "weight": 0.000151522601817907 + }, + { + "days": 0.836806, + "weight": 6.701961234253578e-05 + }, + { + "days": 0.838194, + "weight": 0.000303045203635814 + }, + { + "days": 0.838889, + "weight": 0.0007809241785999821 + }, + { + "days": 0.839583, + "weight": 0.0004254288435656619 + }, + { + "days": 0.840278, + "weight": 8.15890932865653e-05 + }, + { + "days": 0.840972, + "weight": 4.6622339020894455e-05 + }, + { + "days": 0.841667, + "weight": 0.0001996018889332044 + }, + { + "days": 0.842361, + "weight": 9.324467804178891e-05 + }, + { + "days": 0.84375, + "weight": 0.00030013130744700804 + }, + { + "days": 0.844444, + "weight": 0.00013258227659066862 + }, + { + "days": 0.845833, + "weight": 0.0002301977989156664 + }, + { + "days": 0.846528, + "weight": 0.0006250307324988663 + }, + { + "days": 0.847222, + "weight": 0.0008304604138096825 + }, + { + "days": 0.849306, + "weight": 0.0024957520857122563 + }, + { + "days": 0.85, + "weight": 0.00015297954991230994 + }, + { + "days": 0.850694, + "weight": 0.00043271358403767666 + }, + { + "days": 0.851389, + "weight": 0.0002025157851220103 + }, + { + "days": 0.852778, + "weight": 6.701961234253578e-05 + }, + { + "days": 0.854167, + "weight": 7.721824900335645e-05 + }, + { + "days": 0.854861, + "weight": 0.0004866206635305859 + }, + { + "days": 0.855556, + "weight": 0.00021125747368842801 + }, + { + "days": 0.856944, + "weight": 0.0002564228646149195 + }, + { + "days": 0.857639, + "weight": 9.615857423059482e-05 + }, + { + "days": 0.858333, + "weight": 8.74168856641771e-05 + }, + { + "days": 0.860417, + "weight": 0.00020980052559402507 + }, + { + "days": 0.861111, + "weight": 0.00041523020690484124 + }, + { + "days": 0.861806, + "weight": 0.0001238405880242509 + }, + { + "days": 0.863889, + "weight": 0.00034383975027909664 + }, + { + "days": 0.864583, + "weight": 0.00016754903085633945 + }, + { + "days": 0.865278, + "weight": 0.00027099234555894903 + }, + { + "days": 0.865972, + "weight": 0.00015735039419551879 + }, + { + "days": 0.866667, + "weight": 0.00011364195136343023 + }, + { + "days": 0.868056, + "weight": 0.00014715175753469813 + }, + { + "days": 0.86875, + "weight": 0.0002695353974645461 + }, + { + "days": 0.870139, + "weight": 0.00024185338367089 + }, + { + "days": 0.870833, + "weight": 0.0002170852660660398 + }, + { + "days": 0.872222, + "weight": 0.0002083435774996221 + }, + { + "days": 0.882639, + "weight": 9.761552232499777e-05 + }, + { + "days": 0.890278, + "weight": 5.099318330410331e-05 + }, + { + "days": 0.891667, + "weight": 0.0002899326707861874 + }, + { + "days": 0.896528, + "weight": 0.00042251494737685603 + }, + { + "days": 0.897917, + "weight": 9.615857423059482e-05 + }, + { + "days": 0.898611, + "weight": 0.0003817204007335734 + }, + { + "days": 0.901389, + "weight": 0.00022145611034924868 + }, + { + "days": 0.902083, + "weight": 9.907247041940072e-05 + }, + { + "days": 0.902778, + "weight": 0.00020397273321641325 + }, + { + "days": 0.903472, + "weight": 0.0001937740965555926 + }, + { + "days": 0.904861, + "weight": 0.00030013130744700804 + }, + { + "days": 0.905556, + "weight": 0.0032373386657633587 + }, + { + "days": 0.90625, + "weight": 0.0005099318330410331 + }, + { + "days": 0.906944, + "weight": 0.0005259582620794656 + }, + { + "days": 0.907639, + "weight": 0.0003656939716951409 + }, + { + "days": 0.908333, + "weight": 0.00041231631071603534 + }, + { + "days": 0.909722, + "weight": 6.701961234253578e-05 + }, + { + "days": 0.913194, + "weight": 0.0001660920827619365 + }, + { + "days": 0.913889, + "weight": 0.0001996018889332044 + }, + { + "days": 0.914583, + "weight": 0.0003554953350343202 + }, + { + "days": 0.915278, + "weight": 0.00010052941851380368 + }, + { + "days": 0.915972, + "weight": 0.0007372157357678936 + }, + { + "days": 0.916667, + "weight": 0.00035695228312872316 + }, + { + "days": 0.917361, + "weight": 3.059590998246199e-05 + }, + { + "days": 0.918056, + "weight": 0.00018211851180036898 + }, + { + "days": 0.921528, + "weight": 0.009559036447377767 + }, + { + "days": 0.922222, + "weight": 0.0001908602003667867 + }, + { + "days": 0.922917, + "weight": 0.0007707255419391615 + }, + { + "days": 0.923611, + "weight": 0.0002666215012757402 + }, + { + "days": 0.924306, + "weight": 0.0022233027920589045 + }, + { + "days": 0.925, + "weight": 0.0001748337713283542 + }, + { + "days": 0.925694, + "weight": 0.00041231631071603534 + }, + { + "days": 0.926389, + "weight": 8.887383375858005e-05 + }, + { + "days": 0.927083, + "weight": 0.00017046292704514537 + }, + { + "days": 0.930556, + "weight": 0.0004807928711529741 + }, + { + "days": 0.93125, + "weight": 0.0002841048784085756 + }, + { + "days": 0.931944, + "weight": 0.00015735039419551879 + }, + { + "days": 0.932639, + "weight": 0.0002476811760485018 + }, + { + "days": 0.933333, + "weight": 0.0002841048784085756 + }, + { + "days": 0.94375, + "weight": 0.0003788065045447675 + }, + { + "days": 0.948611, + "weight": 0.0001937740965555926 + }, + { + "days": 0.950694, + "weight": 0.00026225065699253135 + }, + { + "days": 0.951389, + "weight": 0.0010402609394037075 + }, + { + "days": 0.955556, + "weight": 0.000662911382953343 + }, + { + "days": 0.95625, + "weight": 0.002363169809121588 + }, + { + "days": 0.956944, + "weight": 0.0008129770366768471 + }, + { + "days": 0.957639, + "weight": 0.00027827708603096377 + }, + { + "days": 0.958333, + "weight": 0.00018940325227238374 + }, + { + "days": 0.959028, + "weight": 0.0004807928711529741 + }, + { + "days": 0.959722, + "weight": 5.536402758731217e-05 + }, + { + "days": 0.960417, + "weight": 0.0029095253445226947 + }, + { + "days": 0.961111, + "weight": 0.0014554911463085488 + }, + { + "days": 0.961806, + "weight": 0.0001908602003667867 + }, + { + "days": 0.9625, + "weight": 0.001117479188407064 + }, + { + "days": 0.964583, + "weight": 0.0009076786628130389 + }, + { + "days": 0.965278, + "weight": 0.00041960105118805013 + }, + { + "days": 0.965972, + "weight": 0.001269001790224971 + }, + { + "days": 0.968056, + "weight": 0.0007299309952958789 + }, + { + "days": 0.96875, + "weight": 0.005730176855286809 + }, + { + "days": 0.970139, + "weight": 0.0003977468297720058 + }, + { + "days": 0.972222, + "weight": 0.0014438355615533252 + }, + { + "days": 0.972917, + "weight": 0.007031231503588645 + }, + { + "days": 0.973611, + "weight": 0.0012471475688089267 + }, + { + "days": 0.974306, + "weight": 0.001295226855924224 + }, + { + "days": 0.975, + "weight": 0.0006221168363100604 + }, + { + "days": 0.975694, + "weight": 0.00020397273321641325 + }, + { + "days": 0.976389, + "weight": 0.0007663546976559527 + }, + { + "days": 0.977083, + "weight": 0.003211113600064106 + }, + { + "days": 0.977778, + "weight": 3.788065045447675e-05 + }, + { + "days": 0.979167, + "weight": 0.0013753590011163865 + }, + { + "days": 0.979861, + "weight": 0.0007022489815022227 + }, + { + "days": 0.980556, + "weight": 0.0010460887317813193 + }, + { + "days": 0.98125, + "weight": 0.0014001271187212368 + }, + { + "days": 0.981944, + "weight": 0.003171776001515226 + }, + { + "days": 0.982639, + "weight": 0.004177070186653263 + }, + { + "days": 0.983333, + "weight": 0.0009732413270611718 + }, + { + "days": 0.984722, + "weight": 0.00033364111361827595 + }, + { + "days": 0.985417, + "weight": 0.0007648977495615497 + }, + { + "days": 0.986111, + "weight": 0.006868053317015514 + }, + { + "days": 0.986806, + "weight": 0.0002637076050869343 + }, + { + "days": 0.988194, + "weight": 0.00863241745933749 + }, + { + "days": 0.988889, + "weight": 0.000906221714718636 + }, + { + "days": 0.989583, + "weight": 0.0003423828021846937 + }, + { + "days": 0.990278, + "weight": 0.0015021134853294433 + }, + { + "days": 0.990972, + "weight": 0.002775486119837623 + }, + { + "days": 0.991667, + "weight": 3.350980617126789e-05 + }, + { + "days": 0.995139, + "weight": 0.0009207911956626656 + }, + { + "days": 0.995833, + "weight": 0.00011801279564663909 + }, + { + "days": 0.998611, + "weight": 0.00045456780545372093 + }, + { + "days": 1.002778, + "weight": 0.0003423828021846937 + }, + { + "days": 1.007639, + "weight": 0.0012354919840537031 + }, + { + "days": 1.008333, + "weight": 3.788065045447675e-05 + }, + { + "days": 1.009722, + "weight": 0.0027944264450648613 + }, + { + "days": 1.011111, + "weight": 0.00034092585409029074 + }, + { + "days": 1.014583, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.015278, + "weight": 0.001994561941237641 + }, + { + "days": 1.016667, + "weight": 0.00195959518697197 + }, + { + "days": 1.017361, + "weight": 0.0015035704334238462 + }, + { + "days": 1.018056, + "weight": 0.01818416916624324 + }, + { + "days": 1.01875, + "weight": 0.0016827750490354093 + }, + { + "days": 1.020139, + "weight": 0.0005026470925690184 + }, + { + "days": 1.021528, + "weight": 0.00015297954991230994 + }, + { + "days": 1.022917, + "weight": 0.001264630945941762 + }, + { + "days": 1.023611, + "weight": 0.00017629071942275716 + }, + { + "days": 1.024306, + "weight": 0.00033946890599588774 + }, + { + "days": 1.025, + "weight": 0.00022437000653805457 + }, + { + "days": 1.027083, + "weight": 0.000664368331047746 + }, + { + "days": 1.027778, + "weight": 0.004127533951443562 + }, + { + "days": 1.028472, + "weight": 8.74168856641771e-05 + }, + { + "days": 1.029861, + "weight": 0.0017293973880563038 + }, + { + "days": 1.03125, + "weight": 0.00019231714846118964 + }, + { + "days": 1.031944, + "weight": 0.001956681290783164 + }, + { + "days": 1.032639, + "weight": 0.001778933623266004 + }, + { + "days": 1.033333, + "weight": 0.0002141713698772339 + }, + { + "days": 1.034028, + "weight": 0.0010708568493861695 + }, + { + "days": 1.034722, + "weight": 0.004765677216792055 + }, + { + "days": 1.035417, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.036111, + "weight": 0.0011320486693510935 + }, + { + "days": 1.036806, + "weight": 0.00023165474701006934 + }, + { + "days": 1.038889, + "weight": 0.0007372157357678936 + }, + { + "days": 1.039583, + "weight": 0.0009411884689843068 + }, + { + "days": 1.040972, + "weight": 0.00048370676734178 + }, + { + "days": 1.041667, + "weight": 0.0005201304697018537 + }, + { + "days": 1.042361, + "weight": 0.0011757571121831822 + }, + { + "days": 1.043056, + "weight": 0.001873635249402196 + }, + { + "days": 1.04375, + "weight": 0.0011014527593686315 + }, + { + "days": 1.044444, + "weight": 0.0004166871549992442 + }, + { + "days": 1.045139, + "weight": 0.0013564186758891481 + }, + { + "days": 1.045833, + "weight": 0.0005492694315899128 + }, + { + "days": 1.046528, + "weight": 0.0006760239158029696 + }, + { + "days": 1.048611, + "weight": 0.0005303291063626745 + }, + { + "days": 1.05, + "weight": 0.000574037549194763 + }, + { + "days": 1.050694, + "weight": 0.000423971895471259 + }, + { + "days": 1.051389, + "weight": 0.02065661008244505 + }, + { + "days": 1.052083, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.052778, + "weight": 4.516539092649151e-05 + }, + { + "days": 1.054861, + "weight": 0.0026982678708342665 + }, + { + "days": 1.056944, + "weight": 0.00030741604791902283 + }, + { + "days": 1.058333, + "weight": 0.00046622339020894457 + }, + { + "days": 1.064583, + "weight": 0.00023165474701006934 + }, + { + "days": 1.065972, + "weight": 6.410571615372988e-05 + }, + { + "days": 1.069444, + "weight": 0.0004137732588104383 + }, + { + "days": 1.073611, + "weight": 0.0003496675426567084 + }, + { + "days": 1.074306, + "weight": 0.0001150988994578332 + }, + { + "days": 1.076389, + "weight": 0.0003117868922022317 + }, + { + "days": 1.077083, + "weight": 0.0002753631898421579 + }, + { + "days": 1.077778, + "weight": 0.0001748337713283542 + }, + { + "days": 1.079167, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.079861, + "weight": 0.00014860870562910107 + }, + { + "days": 1.08125, + "weight": 9.761552232499777e-05 + }, + { + "days": 1.082639, + "weight": 0.0005492694315899128 + }, + { + "days": 1.084028, + "weight": 6.410571615372988e-05 + }, + { + "days": 1.084722, + "weight": 0.0013972132225324306 + }, + { + "days": 1.086111, + "weight": 0.0002025157851220103 + }, + { + "days": 1.0875, + "weight": 6.847656043693873e-05 + }, + { + "days": 1.089583, + "weight": 0.00032198552886305237 + }, + { + "days": 1.090972, + "weight": 0.00018648935608357782 + }, + { + "days": 1.091667, + "weight": 0.0007736394381279674 + }, + { + "days": 1.092361, + "weight": 0.0011713862678999732 + }, + { + "days": 1.09375, + "weight": 0.0004895345597193918 + }, + { + "days": 1.094444, + "weight": 0.00017046292704514537 + }, + { + "days": 1.095139, + "weight": 0.0002476811760485018 + }, + { + "days": 1.097917, + "weight": 0.0013695312087387747 + }, + { + "days": 1.098611, + "weight": 9.324467804178891e-05 + }, + { + "days": 1.1, + "weight": 0.00015297954991230994 + }, + { + "days": 1.100694, + "weight": 0.00043708442832088556 + }, + { + "days": 1.101389, + "weight": 0.0004181441030936472 + }, + { + "days": 1.102083, + "weight": 0.00035695228312872316 + }, + { + "days": 1.104167, + "weight": 0.00020980052559402507 + }, + { + "days": 1.104861, + "weight": 0.00015006565372350402 + }, + { + "days": 1.105556, + "weight": 7.430435281455054e-05 + }, + { + "days": 1.10625, + "weight": 0.00021271442178283096 + }, + { + "days": 1.108333, + "weight": 0.00021562831797163686 + }, + { + "days": 1.109028, + "weight": 6.993350853134168e-05 + }, + { + "days": 1.109722, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.110417, + "weight": 0.00023165474701006934 + }, + { + "days": 1.111806, + "weight": 0.00018940325227238374 + }, + { + "days": 1.1125, + "weight": 0.00022874085082126344 + }, + { + "days": 1.113889, + "weight": 0.0001384100689682804 + }, + { + "days": 1.115972, + "weight": 7.430435281455054e-05 + }, + { + "days": 1.116667, + "weight": 0.00013258227659066862 + }, + { + "days": 1.118056, + "weight": 0.0004603955978313328 + }, + { + "days": 1.120139, + "weight": 0.00042688579166006487 + }, + { + "days": 1.120833, + "weight": 0.009232680074231505 + }, + { + "days": 1.131944, + "weight": 6.701961234253578e-05 + }, + { + "days": 1.136111, + "weight": 0.0001660920827619365 + }, + { + "days": 1.136806, + "weight": 0.00020397273321641325 + }, + { + "days": 1.1375, + "weight": 0.00016172123847872766 + }, + { + "days": 1.138889, + "weight": 0.0003992037778664088 + }, + { + "days": 1.140278, + "weight": 4.516539092649151e-05 + }, + { + "days": 1.141667, + "weight": 8.74168856641771e-05 + }, + { + "days": 1.145833, + "weight": 0.00011946974374104205 + }, + { + "days": 1.146528, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.147222, + "weight": 9.615857423059482e-05 + }, + { + "days": 1.147917, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.148611, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.149306, + "weight": 9.907247041940072e-05 + }, + { + "days": 1.15, + "weight": 0.0001150988994578332 + }, + { + "days": 1.150694, + "weight": 0.0002928465669749933 + }, + { + "days": 1.151389, + "weight": 0.0005113887811354361 + }, + { + "days": 1.152083, + "weight": 0.00010490026279701253 + }, + { + "days": 1.153472, + "weight": 0.00012821143230745975 + }, + { + "days": 1.154861, + "weight": 0.00011364195136343023 + }, + { + "days": 1.155556, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.156944, + "weight": 4.22514947376856e-05 + }, + { + "days": 1.157639, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.159028, + "weight": 0.0003890051412055881 + }, + { + "days": 1.159722, + "weight": 0.00039046208929999107 + }, + { + "days": 1.160417, + "weight": 0.0012194655550152706 + }, + { + "days": 1.161111, + "weight": 0.00023311169510447228 + }, + { + "days": 1.161806, + "weight": 5.9734871870521025e-05 + }, + { + "days": 1.163194, + "weight": 0.00012529753611865386 + }, + { + "days": 1.163889, + "weight": 0.0002957604631637992 + }, + { + "days": 1.165278, + "weight": 0.0002666215012757402 + }, + { + "days": 1.165972, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.166667, + "weight": 4.079454664328265e-05 + }, + { + "days": 1.167361, + "weight": 4.079454664328265e-05 + }, + { + "days": 1.168056, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.170139, + "weight": 6.847656043693873e-05 + }, + { + "days": 1.170833, + "weight": 0.00010490026279701253 + }, + { + "days": 1.171528, + "weight": 0.00013258227659066862 + }, + { + "days": 1.172917, + "weight": 5.3907079492909214e-05 + }, + { + "days": 1.174306, + "weight": 0.00023456864319887523 + }, + { + "days": 1.176389, + "weight": 0.00018940325227238374 + }, + { + "days": 1.177083, + "weight": 0.00016172123847872766 + }, + { + "days": 1.178472, + "weight": 9.178772994738596e-05 + }, + { + "days": 1.181944, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.184722, + "weight": 4.6622339020894455e-05 + }, + { + "days": 1.1875, + "weight": 0.00015589344610111584 + }, + { + "days": 1.189583, + "weight": 2.476811760485018e-05 + }, + { + "days": 1.191667, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.193056, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.19375, + "weight": 0.00014423786134589223 + }, + { + "days": 1.194444, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.198611, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.199306, + "weight": 0.0015895303709936204 + }, + { + "days": 1.200694, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.202778, + "weight": 8.887383375858005e-05 + }, + { + "days": 1.203472, + "weight": 5.3907079492909214e-05 + }, + { + "days": 1.204861, + "weight": 8.304604138096825e-05 + }, + { + "days": 1.205556, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.206944, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.209028, + "weight": 0.00015589344610111584 + }, + { + "days": 1.209722, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.210417, + "weight": 0.0004137732588104383 + }, + { + "days": 1.211111, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.2125, + "weight": 0.00022582695463245752 + }, + { + "days": 1.213194, + "weight": 9.324467804178891e-05 + }, + { + "days": 1.213889, + "weight": 0.00017046292704514537 + }, + { + "days": 1.214583, + "weight": 0.000120926691835445 + }, + { + "days": 1.215278, + "weight": 0.00010781415898581843 + }, + { + "days": 1.215972, + "weight": 4.22514947376856e-05 + }, + { + "days": 1.218056, + "weight": 0.0001238405880242509 + }, + { + "days": 1.219444, + "weight": 0.0001150988994578332 + }, + { + "days": 1.220139, + "weight": 3.788065045447675e-05 + }, + { + "days": 1.220833, + "weight": 0.0007051628776910286 + }, + { + "days": 1.222222, + "weight": 0.00030158825554141104 + }, + { + "days": 1.222917, + "weight": 5.9734871870521025e-05 + }, + { + "days": 1.224306, + "weight": 0.0001150988994578332 + }, + { + "days": 1.225694, + "weight": 0.0008668841161697563 + }, + { + "days": 1.226389, + "weight": 9.907247041940072e-05 + }, + { + "days": 1.228472, + "weight": 0.000120926691835445 + }, + { + "days": 1.229167, + "weight": 4.516539092649151e-05 + }, + { + "days": 1.229861, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.231944, + "weight": 0.00013549617277947452 + }, + { + "days": 1.232639, + "weight": 8.45029894753712e-05 + }, + { + "days": 1.233333, + "weight": 0.00010927110708022139 + }, + { + "days": 1.234028, + "weight": 0.00013986701706268336 + }, + { + "days": 1.234722, + "weight": 7.139045662574464e-05 + }, + { + "days": 1.2375, + "weight": 0.0003788065045447675 + }, + { + "days": 1.238194, + "weight": 7.430435281455054e-05 + }, + { + "days": 1.238889, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.240278, + "weight": 0.00014715175753469813 + }, + { + "days": 1.246528, + "weight": 0.0015603914091055614 + }, + { + "days": 1.251389, + "weight": 6.264876805932693e-05 + }, + { + "days": 1.254167, + "weight": 0.0001544364980067129 + }, + { + "days": 1.25625, + "weight": 0.00010198636660820662 + }, + { + "days": 1.258333, + "weight": 0.00010635721089141548 + }, + { + "days": 1.261806, + "weight": 0.00022874085082126344 + }, + { + "days": 1.263889, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.264583, + "weight": 2.9138961888059035e-05 + }, + { + "days": 1.265972, + "weight": 9.761552232499777e-05 + }, + { + "days": 1.267361, + "weight": 0.0002170852660660398 + }, + { + "days": 1.268056, + "weight": 0.00017191987513954832 + }, + { + "days": 1.26875, + "weight": 0.00011218500326902729 + }, + { + "days": 1.270139, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.272222, + "weight": 9.178772994738596e-05 + }, + { + "days": 1.273611, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.274306, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.275, + "weight": 0.0001238405880242509 + }, + { + "days": 1.276389, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.277083, + "weight": 0.00013695312087387746 + }, + { + "days": 1.278472, + "weight": 8.45029894753712e-05 + }, + { + "days": 1.279167, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.279861, + "weight": 4.079454664328265e-05 + }, + { + "days": 1.281944, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.282639, + "weight": 0.00028119098221976967 + }, + { + "days": 1.283333, + "weight": 9.761552232499777e-05 + }, + { + "days": 1.284028, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.284722, + "weight": 0.00033946890599588774 + }, + { + "days": 1.285417, + "weight": 0.0001267544842130568 + }, + { + "days": 1.286111, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.288194, + "weight": 0.0002928465669749933 + }, + { + "days": 1.290278, + "weight": 4.079454664328265e-05 + }, + { + "days": 1.29375, + "weight": 0.00012238363992984796 + }, + { + "days": 1.294444, + "weight": 0.00010490026279701253 + }, + { + "days": 1.295139, + "weight": 3.788065045447675e-05 + }, + { + "days": 1.295833, + "weight": 4.6622339020894455e-05 + }, + { + "days": 1.296528, + "weight": 0.000151522601817907 + }, + { + "days": 1.297917, + "weight": 0.00010490026279701253 + }, + { + "days": 1.298611, + "weight": 0.0004705942344921534 + }, + { + "days": 1.299306, + "weight": 0.00013549617277947452 + }, + { + "days": 1.303472, + "weight": 0.00022728390272686047 + }, + { + "days": 1.306944, + "weight": 9.470162613619187e-05 + }, + { + "days": 1.313194, + "weight": 8.595993756977416e-05 + }, + { + "days": 1.315972, + "weight": 5.9734871870521025e-05 + }, + { + "days": 1.316667, + "weight": 4.516539092649151e-05 + }, + { + "days": 1.317361, + "weight": 4.22514947376856e-05 + }, + { + "days": 1.322917, + "weight": 8.595993756977416e-05 + }, + { + "days": 1.325, + "weight": 6.410571615372988e-05 + }, + { + "days": 1.325694, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.326389, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.327083, + "weight": 8.304604138096825e-05 + }, + { + "days": 1.327778, + "weight": 2.7682013793656084e-05 + }, + { + "days": 1.328472, + "weight": 0.00014715175753469813 + }, + { + "days": 1.329167, + "weight": 4.516539092649151e-05 + }, + { + "days": 1.329861, + "weight": 8.304604138096825e-05 + }, + { + "days": 1.330556, + "weight": 9.033078185298302e-05 + }, + { + "days": 1.33125, + "weight": 0.0005813222896667778 + }, + { + "days": 1.332639, + "weight": 9.907247041940072e-05 + }, + { + "days": 1.333333, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.334722, + "weight": 0.00010927110708022139 + }, + { + "days": 1.336806, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.338194, + "weight": 9.907247041940072e-05 + }, + { + "days": 1.338889, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.339583, + "weight": 7.57613009089535e-05 + }, + { + "days": 1.340278, + "weight": 4.516539092649151e-05 + }, + { + "days": 1.340972, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.342361, + "weight": 0.0003234424769574553 + }, + { + "days": 1.343056, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.344444, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.345139, + "weight": 0.00010490026279701253 + }, + { + "days": 1.346528, + "weight": 0.0001748337713283542 + }, + { + "days": 1.348611, + "weight": 0.0001908602003667867 + }, + { + "days": 1.35, + "weight": 0.00016754903085633945 + }, + { + "days": 1.352083, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.352778, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.353472, + "weight": 8.595993756977416e-05 + }, + { + "days": 1.354167, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.354861, + "weight": 5.536402758731217e-05 + }, + { + "days": 1.355556, + "weight": 8.887383375858005e-05 + }, + { + "days": 1.357639, + "weight": 0.00013258227659066862 + }, + { + "days": 1.360417, + "weight": 0.0004647664421145416 + }, + { + "days": 1.361111, + "weight": 9.470162613619187e-05 + }, + { + "days": 1.363194, + "weight": 0.00010052941851380368 + }, + { + "days": 1.372222, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.372917, + "weight": 0.00010344331470260957 + }, + { + "days": 1.377083, + "weight": 4.6622339020894455e-05 + }, + { + "days": 1.379861, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.382639, + "weight": 8.304604138096825e-05 + }, + { + "days": 1.385417, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.386111, + "weight": 3.496675426567084e-05 + }, + { + "days": 1.386806, + "weight": 4.079454664328265e-05 + }, + { + "days": 1.388194, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.388889, + "weight": 3.788065045447675e-05 + }, + { + "days": 1.389583, + "weight": 7.430435281455054e-05 + }, + { + "days": 1.390972, + "weight": 5.9734871870521025e-05 + }, + { + "days": 1.391667, + "weight": 9.178772994738596e-05 + }, + { + "days": 1.392361, + "weight": 0.00011218500326902729 + }, + { + "days": 1.393056, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.395139, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.396528, + "weight": 2.7682013793656084e-05 + }, + { + "days": 1.397222, + "weight": 0.0002564228646149195 + }, + { + "days": 1.397917, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.398611, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.399306, + "weight": 0.0003686078678839468 + }, + { + "days": 1.4, + "weight": 7.139045662574464e-05 + }, + { + "days": 1.402083, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.402778, + "weight": 2.476811760485018e-05 + }, + { + "days": 1.403472, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.404167, + "weight": 0.0002695353974645461 + }, + { + "days": 1.405556, + "weight": 3.496675426567084e-05 + }, + { + "days": 1.407639, + "weight": 9.761552232499777e-05 + }, + { + "days": 1.409028, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.409722, + "weight": 2.7682013793656084e-05 + }, + { + "days": 1.410417, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.4125, + "weight": 0.00013258227659066862 + }, + { + "days": 1.413194, + "weight": 0.00010052941851380368 + }, + { + "days": 1.414583, + "weight": 0.00019814494083880143 + }, + { + "days": 1.415972, + "weight": 2.9138961888059035e-05 + }, + { + "days": 1.418056, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.41875, + "weight": 4.807928711529741e-05 + }, + { + "days": 1.419444, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.420139, + "weight": 0.0003059590998246199 + }, + { + "days": 1.426389, + "weight": 9.615857423059482e-05 + }, + { + "days": 1.430556, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.435417, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.4375, + "weight": 3.496675426567084e-05 + }, + { + "days": 1.440278, + "weight": 3.788065045447675e-05 + }, + { + "days": 1.441667, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.445833, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.447222, + "weight": 3.788065045447675e-05 + }, + { + "days": 1.448611, + "weight": 0.0002535089684261136 + }, + { + "days": 1.449306, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.45, + "weight": 4.516539092649151e-05 + }, + { + "days": 1.451389, + "weight": 0.0001267544842130568 + }, + { + "days": 1.453472, + "weight": 6.410571615372988e-05 + }, + { + "days": 1.454167, + "weight": 0.00017629071942275716 + }, + { + "days": 1.45625, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.456944, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.458333, + "weight": 0.00011655584755223614 + }, + { + "days": 1.460417, + "weight": 0.00026225065699253135 + }, + { + "days": 1.461111, + "weight": 0.00023456864319887523 + }, + { + "days": 1.463889, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.464583, + "weight": 6.993350853134168e-05 + }, + { + "days": 1.465972, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.468056, + "weight": 5.536402758731217e-05 + }, + { + "days": 1.469444, + "weight": 5.536402758731217e-05 + }, + { + "days": 1.470139, + "weight": 0.00018940325227238374 + }, + { + "days": 1.471528, + "weight": 0.000120926691835445 + }, + { + "days": 1.472222, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.472917, + "weight": 4.079454664328265e-05 + }, + { + "days": 1.474306, + "weight": 4.807928711529741e-05 + }, + { + "days": 1.475694, + "weight": 6.701961234253578e-05 + }, + { + "days": 1.476389, + "weight": 3.496675426567084e-05 + }, + { + "days": 1.477778, + "weight": 8.304604138096825e-05 + }, + { + "days": 1.479167, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.48125, + "weight": 0.00034383975027909664 + }, + { + "days": 1.482639, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.495833, + "weight": 5.9734871870521025e-05 + }, + { + "days": 1.498611, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.499306, + "weight": 0.00017046292704514537 + }, + { + "days": 1.504167, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.507639, + "weight": 7.57613009089535e-05 + }, + { + "days": 1.508333, + "weight": 7.430435281455054e-05 + }, + { + "days": 1.509028, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.509722, + "weight": 8.595993756977416e-05 + }, + { + "days": 1.510417, + "weight": 2.7682013793656084e-05 + }, + { + "days": 1.513194, + "weight": 9.615857423059482e-05 + }, + { + "days": 1.513889, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.515278, + "weight": 2.9138961888059035e-05 + }, + { + "days": 1.518056, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.51875, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.519444, + "weight": 9.761552232499777e-05 + }, + { + "days": 1.520139, + "weight": 0.0003948329335831999 + }, + { + "days": 1.521528, + "weight": 9.033078185298302e-05 + }, + { + "days": 1.522222, + "weight": 0.00013986701706268336 + }, + { + "days": 1.523611, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.524306, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.527083, + "weight": 0.00010927110708022139 + }, + { + "days": 1.527778, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.528472, + "weight": 6.264876805932693e-05 + }, + { + "days": 1.529167, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.530556, + "weight": 3.788065045447675e-05 + }, + { + "days": 1.53125, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.532639, + "weight": 0.00010781415898581843 + }, + { + "days": 1.534028, + "weight": 5.9734871870521025e-05 + }, + { + "days": 1.534722, + "weight": 6.993350853134168e-05 + }, + { + "days": 1.535417, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.536111, + "weight": 6.264876805932693e-05 + }, + { + "days": 1.536806, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.538194, + "weight": 0.00036715091978954385 + }, + { + "days": 1.545833, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.550694, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.55625, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.558333, + "weight": 0.0003452966983734996 + }, + { + "days": 1.564583, + "weight": 3.059590998246199e-05 + }, + { + "days": 1.565972, + "weight": 4.079454664328265e-05 + }, + { + "days": 1.567361, + "weight": 4.516539092649151e-05 + }, + { + "days": 1.568056, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.570139, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.572917, + "weight": 0.00011946974374104205 + }, + { + "days": 1.574306, + "weight": 6.410571615372988e-05 + }, + { + "days": 1.577778, + "weight": 3.788065045447675e-05 + }, + { + "days": 1.578472, + "weight": 4.22514947376856e-05 + }, + { + "days": 1.579167, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.580556, + "weight": 0.0001238405880242509 + }, + { + "days": 1.581944, + "weight": 0.0004050315702440206 + }, + { + "days": 1.582639, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.583333, + "weight": 6.410571615372988e-05 + }, + { + "days": 1.585417, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.586806, + "weight": 2.9138961888059035e-05 + }, + { + "days": 1.5875, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.588194, + "weight": 0.00012821143230745975 + }, + { + "days": 1.590972, + "weight": 8.45029894753712e-05 + }, + { + "days": 1.592361, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.593056, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.594444, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.595833, + "weight": 0.00033072721742947005 + }, + { + "days": 1.596528, + "weight": 6.264876805932693e-05 + }, + { + "days": 1.597917, + "weight": 6.410571615372988e-05 + }, + { + "days": 1.599306, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.6, + "weight": 2.7682013793656084e-05 + }, + { + "days": 1.601389, + "weight": 2.476811760485018e-05 + }, + { + "days": 1.615278, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.621528, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.625, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.627083, + "weight": 3.059590998246199e-05 + }, + { + "days": 1.634722, + "weight": 8.45029894753712e-05 + }, + { + "days": 1.6375, + "weight": 2.9138961888059035e-05 + }, + { + "days": 1.639583, + "weight": 5.9734871870521025e-05 + }, + { + "days": 1.640972, + "weight": 6.264876805932693e-05 + }, + { + "days": 1.641667, + "weight": 2.7682013793656084e-05 + }, + { + "days": 1.642361, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.64375, + "weight": 0.00013695312087387746 + }, + { + "days": 1.644444, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.645833, + "weight": 5.536402758731217e-05 + }, + { + "days": 1.646528, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.647917, + "weight": 0.00017920461561156306 + }, + { + "days": 1.650694, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.653472, + "weight": 8.15890932865653e-05 + }, + { + "days": 1.654861, + "weight": 0.00017920461561156306 + }, + { + "days": 1.655556, + "weight": 0.00013403922468507157 + }, + { + "days": 1.65625, + "weight": 4.807928711529741e-05 + }, + { + "days": 1.656944, + "weight": 9.761552232499777e-05 + }, + { + "days": 1.658333, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.659722, + "weight": 0.0003846342969223793 + }, + { + "days": 1.6625, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.668056, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.670833, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.671528, + "weight": 0.00011218500326902729 + }, + { + "days": 1.676389, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.684028, + "weight": 2.7682013793656084e-05 + }, + { + "days": 1.689583, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.691667, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.69375, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.696528, + "weight": 3.496675426567084e-05 + }, + { + "days": 1.697917, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.698611, + "weight": 3.059590998246199e-05 + }, + { + "days": 1.699306, + "weight": 7.57613009089535e-05 + }, + { + "days": 1.700694, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.703472, + "weight": 8.595993756977416e-05 + }, + { + "days": 1.704861, + "weight": 3.496675426567084e-05 + }, + { + "days": 1.705556, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.709722, + "weight": 0.00015880734228992173 + }, + { + "days": 1.711806, + "weight": 9.761552232499777e-05 + }, + { + "days": 1.7125, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.713889, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.714583, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.715278, + "weight": 2.7682013793656084e-05 + }, + { + "days": 1.715972, + "weight": 8.304604138096825e-05 + }, + { + "days": 1.716667, + "weight": 0.00015297954991230994 + }, + { + "days": 1.71875, + "weight": 3.496675426567084e-05 + }, + { + "days": 1.720139, + "weight": 0.000633772421065284 + }, + { + "days": 1.721528, + "weight": 5.2450131398506267e-05 + }, + { + "days": 1.731944, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.735417, + "weight": 9.324467804178891e-05 + }, + { + "days": 1.738889, + "weight": 5.9734871870521025e-05 + }, + { + "days": 1.743056, + "weight": 3.059590998246199e-05 + }, + { + "days": 1.744444, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.748611, + "weight": 4.079454664328265e-05 + }, + { + "days": 1.757639, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.758333, + "weight": 5.827792377611807e-05 + }, + { + "days": 1.760417, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.761111, + "weight": 6.993350853134168e-05 + }, + { + "days": 1.7625, + "weight": 0.00017337682323395127 + }, + { + "days": 1.763194, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.766667, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.772222, + "weight": 6.993350853134168e-05 + }, + { + "days": 1.773611, + "weight": 0.00011655584755223614 + }, + { + "days": 1.775, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.775694, + "weight": 8.15890932865653e-05 + }, + { + "days": 1.777083, + "weight": 7.721824900335645e-05 + }, + { + "days": 1.777778, + "weight": 8.013214519216235e-05 + }, + { + "days": 1.779167, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.781944, + "weight": 0.0001602642903843247 + }, + { + "days": 1.782639, + "weight": 0.000543441639212301 + }, + { + "days": 1.789583, + "weight": 0.00010198636660820662 + }, + { + "days": 1.810417, + "weight": 4.807928711529741e-05 + }, + { + "days": 1.813194, + "weight": 3.059590998246199e-05 + }, + { + "days": 1.815278, + "weight": 3.205285807686494e-05 + }, + { + "days": 1.819444, + "weight": 8.887383375858005e-05 + }, + { + "days": 1.820833, + "weight": 3.059590998246199e-05 + }, + { + "days": 1.821528, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.823611, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.825, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.825694, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.827083, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.828472, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.829167, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.83125, + "weight": 4.6622339020894455e-05 + }, + { + "days": 1.831944, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.834028, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.834722, + "weight": 9.033078185298302e-05 + }, + { + "days": 1.835417, + "weight": 3.496675426567084e-05 + }, + { + "days": 1.836806, + "weight": 0.0001631781865731306 + }, + { + "days": 1.8375, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.838889, + "weight": 6.993350853134168e-05 + }, + { + "days": 1.839583, + "weight": 4.22514947376856e-05 + }, + { + "days": 1.840278, + "weight": 7.430435281455054e-05 + }, + { + "days": 1.840972, + "weight": 0.0001267544842130568 + }, + { + "days": 1.844444, + "weight": 0.0005638389125339424 + }, + { + "days": 1.845833, + "weight": 3.788065045447675e-05 + }, + { + "days": 1.847917, + "weight": 6.701961234253578e-05 + }, + { + "days": 1.849306, + "weight": 4.6622339020894455e-05 + }, + { + "days": 1.85625, + "weight": 7.867519709775939e-05 + }, + { + "days": 1.857639, + "weight": 3.059590998246199e-05 + }, + { + "days": 1.863889, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.867361, + "weight": 3.059590998246199e-05 + }, + { + "days": 1.868056, + "weight": 4.6622339020894455e-05 + }, + { + "days": 1.872222, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.872917, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.878472, + "weight": 9.324467804178891e-05 + }, + { + "days": 1.880556, + "weight": 3.9337598548879697e-05 + }, + { + "days": 1.88125, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.882639, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.883333, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.884722, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.885417, + "weight": 2.9138961888059035e-05 + }, + { + "days": 1.886806, + "weight": 2.9138961888059035e-05 + }, + { + "days": 1.888194, + "weight": 5.3907079492909214e-05 + }, + { + "days": 1.890278, + "weight": 0.00022437000653805457 + }, + { + "days": 1.891667, + "weight": 9.033078185298302e-05 + }, + { + "days": 1.894444, + "weight": 4.6622339020894455e-05 + }, + { + "days": 1.895833, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.897222, + "weight": 6.264876805932693e-05 + }, + { + "days": 1.897917, + "weight": 0.00022437000653805457 + }, + { + "days": 1.898611, + "weight": 7.284740472014759e-05 + }, + { + "days": 1.899306, + "weight": 0.0001544364980067129 + }, + { + "days": 1.9, + "weight": 4.079454664328265e-05 + }, + { + "days": 1.900694, + "weight": 0.00011072805517462434 + }, + { + "days": 1.906944, + "weight": 8.887383375858005e-05 + }, + { + "days": 1.907639, + "weight": 0.0008741688566417711 + }, + { + "days": 1.908333, + "weight": 4.516539092649151e-05 + }, + { + "days": 1.909722, + "weight": 4.6622339020894455e-05 + }, + { + "days": 1.916667, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.921528, + "weight": 2.476811760485018e-05 + }, + { + "days": 1.922917, + "weight": 6.264876805932693e-05 + }, + { + "days": 1.923611, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.925694, + "weight": 2.7682013793656084e-05 + }, + { + "days": 1.932639, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.933333, + "weight": 0.00010781415898581843 + }, + { + "days": 1.936111, + "weight": 3.6423702360073794e-05 + }, + { + "days": 1.939583, + "weight": 4.6622339020894455e-05 + }, + { + "days": 1.940278, + "weight": 0.00010198636660820662 + }, + { + "days": 1.941667, + "weight": 3.059590998246199e-05 + }, + { + "days": 1.942361, + "weight": 8.595993756977416e-05 + }, + { + "days": 1.943056, + "weight": 4.807928711529741e-05 + }, + { + "days": 1.945139, + "weight": 4.807928711529741e-05 + }, + { + "days": 1.945833, + "weight": 9.033078185298302e-05 + }, + { + "days": 1.946528, + "weight": 0.00014278091325148928 + }, + { + "days": 1.947222, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.947917, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.949306, + "weight": 6.119181996492398e-05 + }, + { + "days": 1.95, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.950694, + "weight": 8.15890932865653e-05 + }, + { + "days": 1.951389, + "weight": 8.887383375858005e-05 + }, + { + "days": 1.954861, + "weight": 0.00018794630417798077 + }, + { + "days": 1.955556, + "weight": 3.350980617126789e-05 + }, + { + "days": 1.956944, + "weight": 0.00012821143230745975 + }, + { + "days": 1.958333, + "weight": 5.536402758731217e-05 + }, + { + "days": 1.959028, + "weight": 0.00037006481597834975 + }, + { + "days": 1.959722, + "weight": 0.00015006565372350402 + }, + { + "days": 1.961111, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.963194, + "weight": 2.6225065699253133e-05 + }, + { + "days": 1.963889, + "weight": 4.079454664328265e-05 + }, + { + "days": 1.964583, + "weight": 0.00018503240798917487 + }, + { + "days": 1.965972, + "weight": 0.0003656939716951409 + }, + { + "days": 1.966667, + "weight": 4.370844283208855e-05 + }, + { + "days": 1.968056, + "weight": 4.22514947376856e-05 + }, + { + "days": 1.970139, + "weight": 4.953623520970036e-05 + }, + { + "days": 1.970833, + "weight": 4.22514947376856e-05 + }, + { + "days": 1.972917, + "weight": 8.45029894753712e-05 + }, + { + "days": 1.975694, + "weight": 3.496675426567084e-05 + }, + { + "days": 1.98125, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.981944, + "weight": 6.556266424813284e-05 + }, + { + "days": 1.982639, + "weight": 0.00016172123847872766 + }, + { + "days": 1.9875, + "weight": 8.45029894753712e-05 + }, + { + "days": 1.988194, + "weight": 8.887383375858005e-05 + }, + { + "days": 1.990972, + "weight": 5.3907079492909214e-05 + }, + { + "days": 1.99375, + "weight": 9.615857423059482e-05 + }, + { + "days": 1.997222, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.997917, + "weight": 5.6820975681715117e-05 + }, + { + "days": 1.998611, + "weight": 0.00022437000653805457 + }, + { + "days": 1.999306, + "weight": 7.57613009089535e-05 + }, + { + "days": 2.001389, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.002083, + "weight": 0.00010344331470260957 + }, + { + "days": 2.004167, + "weight": 0.00034092585409029074 + }, + { + "days": 2.004861, + "weight": 0.0001748337713283542 + }, + { + "days": 2.005556, + "weight": 3.059590998246199e-05 + }, + { + "days": 2.006944, + "weight": 3.205285807686494e-05 + }, + { + "days": 2.008333, + "weight": 7.284740472014759e-05 + }, + { + "days": 2.009028, + "weight": 0.00017191987513954832 + }, + { + "days": 2.009722, + "weight": 0.00014860870562910107 + }, + { + "days": 2.010417, + "weight": 0.00012238363992984796 + }, + { + "days": 2.013194, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.014583, + "weight": 6.119181996492398e-05 + }, + { + "days": 2.015278, + "weight": 4.807928711529741e-05 + }, + { + "days": 2.015972, + "weight": 0.00023311169510447228 + }, + { + "days": 2.018056, + "weight": 5.2450131398506267e-05 + }, + { + "days": 2.01875, + "weight": 8.74168856641771e-05 + }, + { + "days": 2.020139, + "weight": 0.00010781415898581843 + }, + { + "days": 2.020833, + "weight": 0.00030450215173021694 + }, + { + "days": 2.023611, + "weight": 0.00010781415898581843 + }, + { + "days": 2.024306, + "weight": 5.3907079492909214e-05 + }, + { + "days": 2.025694, + "weight": 0.0004560247535481239 + }, + { + "days": 2.03125, + "weight": 0.000272449293653352 + }, + { + "days": 2.031944, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.034028, + "weight": 8.304604138096825e-05 + }, + { + "days": 2.041667, + "weight": 5.6820975681715117e-05 + }, + { + "days": 2.042361, + "weight": 8.304604138096825e-05 + }, + { + "days": 2.044444, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.045139, + "weight": 4.516539092649151e-05 + }, + { + "days": 2.046528, + "weight": 4.079454664328265e-05 + }, + { + "days": 2.048611, + "weight": 4.22514947376856e-05 + }, + { + "days": 2.049306, + "weight": 6.556266424813284e-05 + }, + { + "days": 2.052778, + "weight": 7.721824900335645e-05 + }, + { + "days": 2.053472, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.054167, + "weight": 3.205285807686494e-05 + }, + { + "days": 2.056944, + "weight": 8.887383375858005e-05 + }, + { + "days": 2.057639, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.059028, + "weight": 3.205285807686494e-05 + }, + { + "days": 2.059722, + "weight": 6.556266424813284e-05 + }, + { + "days": 2.063889, + "weight": 0.00010344331470260957 + }, + { + "days": 2.065278, + "weight": 5.3907079492909214e-05 + }, + { + "days": 2.065972, + "weight": 3.205285807686494e-05 + }, + { + "days": 2.066667, + "weight": 6.264876805932693e-05 + }, + { + "days": 2.068056, + "weight": 9.615857423059482e-05 + }, + { + "days": 2.070833, + "weight": 0.00014715175753469813 + }, + { + "days": 2.071528, + "weight": 5.6820975681715117e-05 + }, + { + "days": 2.072917, + "weight": 6.993350853134168e-05 + }, + { + "days": 2.073611, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.075, + "weight": 0.00014715175753469813 + }, + { + "days": 2.077083, + "weight": 0.00010490026279701253 + }, + { + "days": 2.078472, + "weight": 8.887383375858005e-05 + }, + { + "days": 2.079861, + "weight": 7.57613009089535e-05 + }, + { + "days": 2.080556, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.082639, + "weight": 6.264876805932693e-05 + }, + { + "days": 2.083333, + "weight": 2.476811760485018e-05 + }, + { + "days": 2.086806, + "weight": 0.0003205285807686494 + }, + { + "days": 2.093056, + "weight": 0.00045165390926491504 + }, + { + "days": 2.095139, + "weight": 3.059590998246199e-05 + }, + { + "days": 2.108333, + "weight": 6.993350853134168e-05 + }, + { + "days": 2.110417, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.115972, + "weight": 4.079454664328265e-05 + }, + { + "days": 2.117361, + "weight": 2.476811760485018e-05 + }, + { + "days": 2.11875, + "weight": 0.0001544364980067129 + }, + { + "days": 2.120833, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.122917, + "weight": 2.6225065699253133e-05 + }, + { + "days": 2.125, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.126389, + "weight": 3.059590998246199e-05 + }, + { + "days": 2.127778, + "weight": 5.827792377611807e-05 + }, + { + "days": 2.129861, + "weight": 4.079454664328265e-05 + }, + { + "days": 2.130556, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.132639, + "weight": 2.6225065699253133e-05 + }, + { + "days": 2.134028, + "weight": 5.2450131398506267e-05 + }, + { + "days": 2.136111, + "weight": 9.324467804178891e-05 + }, + { + "days": 2.1375, + "weight": 0.00013403922468507157 + }, + { + "days": 2.138194, + "weight": 7.57613009089535e-05 + }, + { + "days": 2.138889, + "weight": 3.788065045447675e-05 + }, + { + "days": 2.140278, + "weight": 0.0001384100689682804 + }, + { + "days": 2.140972, + "weight": 4.22514947376856e-05 + }, + { + "days": 2.145139, + "weight": 3.788065045447675e-05 + }, + { + "days": 2.15, + "weight": 0.00010927110708022139 + }, + { + "days": 2.152778, + "weight": 2.6225065699253133e-05 + }, + { + "days": 2.154861, + "weight": 0.0001660920827619365 + }, + { + "days": 2.159028, + "weight": 9.033078185298302e-05 + }, + { + "days": 2.164583, + "weight": 3.205285807686494e-05 + }, + { + "days": 2.165972, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.168056, + "weight": 2.7682013793656084e-05 + }, + { + "days": 2.178472, + "weight": 2.476811760485018e-05 + }, + { + "days": 2.180556, + "weight": 5.2450131398506267e-05 + }, + { + "days": 2.181944, + "weight": 9.615857423059482e-05 + }, + { + "days": 2.182639, + "weight": 4.807928711529741e-05 + }, + { + "days": 2.184722, + "weight": 9.761552232499777e-05 + }, + { + "days": 2.188194, + "weight": 3.059590998246199e-05 + }, + { + "days": 2.189583, + "weight": 5.3907079492909214e-05 + }, + { + "days": 2.190972, + "weight": 5.827792377611807e-05 + }, + { + "days": 2.191667, + "weight": 3.205285807686494e-05 + }, + { + "days": 2.195139, + "weight": 5.536402758731217e-05 + }, + { + "days": 2.196528, + "weight": 2.6225065699253133e-05 + }, + { + "days": 2.197222, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.197917, + "weight": 3.205285807686494e-05 + }, + { + "days": 2.198611, + "weight": 5.9734871870521025e-05 + }, + { + "days": 2.199306, + "weight": 4.953623520970036e-05 + }, + { + "days": 2.2, + "weight": 5.6820975681715117e-05 + }, + { + "days": 2.204167, + "weight": 3.496675426567084e-05 + }, + { + "days": 2.206944, + "weight": 0.0001238405880242509 + }, + { + "days": 2.210417, + "weight": 0.00023165474701006934 + }, + { + "days": 2.211806, + "weight": 2.476811760485018e-05 + }, + { + "days": 2.220833, + "weight": 0.00011801279564663909 + }, + { + "days": 2.238889, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.241667, + "weight": 4.22514947376856e-05 + }, + { + "days": 2.24375, + "weight": 5.9734871870521025e-05 + }, + { + "days": 2.245139, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.246528, + "weight": 4.516539092649151e-05 + }, + { + "days": 2.247917, + "weight": 8.15890932865653e-05 + }, + { + "days": 2.250694, + "weight": 5.2450131398506267e-05 + }, + { + "days": 2.252778, + "weight": 4.6622339020894455e-05 + }, + { + "days": 2.253472, + "weight": 3.496675426567084e-05 + }, + { + "days": 2.254167, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.25625, + "weight": 6.993350853134168e-05 + }, + { + "days": 2.256944, + "weight": 0.00011364195136343023 + }, + { + "days": 2.258333, + "weight": 3.496675426567084e-05 + }, + { + "days": 2.259028, + "weight": 4.516539092649151e-05 + }, + { + "days": 2.261806, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.266667, + "weight": 8.595993756977416e-05 + }, + { + "days": 2.269444, + "weight": 0.00019231714846118964 + }, + { + "days": 2.272222, + "weight": 0.0001602642903843247 + }, + { + "days": 2.299306, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.302778, + "weight": 4.953623520970036e-05 + }, + { + "days": 2.309722, + "weight": 3.788065045447675e-05 + }, + { + "days": 2.311806, + "weight": 6.119181996492398e-05 + }, + { + "days": 2.3125, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.313194, + "weight": 2.7682013793656084e-05 + }, + { + "days": 2.314583, + "weight": 5.3907079492909214e-05 + }, + { + "days": 2.315972, + "weight": 5.3907079492909214e-05 + }, + { + "days": 2.318056, + "weight": 0.00010927110708022139 + }, + { + "days": 2.31875, + "weight": 4.6622339020894455e-05 + }, + { + "days": 2.325694, + "weight": 8.304604138096825e-05 + }, + { + "days": 2.330556, + "weight": 0.0002170852660660398 + }, + { + "days": 2.33125, + "weight": 0.00010781415898581843 + }, + { + "days": 2.336111, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.338194, + "weight": 0.00010490026279701253 + }, + { + "days": 2.360417, + "weight": 4.6622339020894455e-05 + }, + { + "days": 2.36875, + "weight": 4.22514947376856e-05 + }, + { + "days": 2.373611, + "weight": 9.033078185298302e-05 + }, + { + "days": 2.375, + "weight": 2.7682013793656084e-05 + }, + { + "days": 2.376389, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.379167, + "weight": 6.701961234253578e-05 + }, + { + "days": 2.379861, + "weight": 2.7682013793656084e-05 + }, + { + "days": 2.384722, + "weight": 0.00014715175753469813 + }, + { + "days": 2.385417, + "weight": 4.22514947376856e-05 + }, + { + "days": 2.386111, + "weight": 0.0002301977989156664 + }, + { + "days": 2.392361, + "weight": 0.0001238405880242509 + }, + { + "days": 2.39375, + "weight": 2.6225065699253133e-05 + }, + { + "days": 2.397222, + "weight": 6.847656043693873e-05 + }, + { + "days": 2.421528, + "weight": 4.079454664328265e-05 + }, + { + "days": 2.43125, + "weight": 6.847656043693873e-05 + }, + { + "days": 2.432639, + "weight": 5.3907079492909214e-05 + }, + { + "days": 2.434722, + "weight": 6.993350853134168e-05 + }, + { + "days": 2.435417, + "weight": 2.476811760485018e-05 + }, + { + "days": 2.436806, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.4375, + "weight": 3.496675426567084e-05 + }, + { + "days": 2.440278, + "weight": 0.00015297954991230994 + }, + { + "days": 2.44375, + "weight": 3.496675426567084e-05 + }, + { + "days": 2.447917, + "weight": 0.00010635721089141548 + }, + { + "days": 2.448611, + "weight": 3.496675426567084e-05 + }, + { + "days": 2.450694, + "weight": 0.00023748253938768113 + }, + { + "days": 2.451389, + "weight": 4.953623520970036e-05 + }, + { + "days": 2.458333, + "weight": 0.000120926691835445 + }, + { + "days": 2.478472, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.49375, + "weight": 3.496675426567084e-05 + }, + { + "days": 2.495139, + "weight": 2.6225065699253133e-05 + }, + { + "days": 2.495833, + "weight": 6.410571615372988e-05 + }, + { + "days": 2.496528, + "weight": 5.536402758731217e-05 + }, + { + "days": 2.499306, + "weight": 0.00023748253938768113 + }, + { + "days": 2.501389, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.505556, + "weight": 0.0003190716326742464 + }, + { + "days": 2.509722, + "weight": 5.6820975681715117e-05 + }, + { + "days": 2.511111, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.5125, + "weight": 0.00012238363992984796 + }, + { + "days": 2.513194, + "weight": 3.9337598548879697e-05 + }, + { + "days": 2.513889, + "weight": 9.324467804178891e-05 + }, + { + "days": 2.532639, + "weight": 3.205285807686494e-05 + }, + { + "days": 2.538194, + "weight": 3.496675426567084e-05 + }, + { + "days": 2.552083, + "weight": 2.6225065699253133e-05 + }, + { + "days": 2.554167, + "weight": 4.516539092649151e-05 + }, + { + "days": 2.557639, + "weight": 3.496675426567084e-05 + }, + { + "days": 2.561111, + "weight": 2.7682013793656084e-05 + }, + { + "days": 2.5625, + "weight": 5.2450131398506267e-05 + }, + { + "days": 2.565278, + "weight": 6.264876805932693e-05 + }, + { + "days": 2.565972, + "weight": 0.0004079454664328265 + }, + { + "days": 2.567361, + "weight": 0.00018357545989477193 + }, + { + "days": 2.568056, + "weight": 4.807928711529741e-05 + }, + { + "days": 2.570139, + "weight": 7.139045662574464e-05 + }, + { + "days": 2.570833, + "weight": 0.00017337682323395127 + }, + { + "days": 2.578472, + "weight": 0.0001296683804018627 + }, + { + "days": 2.602083, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.620139, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.621528, + "weight": 0.00015880734228992173 + }, + { + "days": 2.628472, + "weight": 0.00016172123847872766 + }, + { + "days": 2.629167, + "weight": 8.15890932865653e-05 + }, + { + "days": 2.629861, + "weight": 0.00010635721089141548 + }, + { + "days": 2.63125, + "weight": 5.536402758731217e-05 + }, + { + "days": 2.632639, + "weight": 0.0001602642903843247 + }, + { + "days": 2.633333, + "weight": 0.00014132396515708633 + }, + { + "days": 2.636806, + "weight": 0.0004997331963802125 + }, + { + "days": 2.672917, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.674306, + "weight": 5.2450131398506267e-05 + }, + { + "days": 2.680556, + "weight": 0.0002447672798596959 + }, + { + "days": 2.684722, + "weight": 9.907247041940072e-05 + }, + { + "days": 2.686806, + "weight": 7.284740472014759e-05 + }, + { + "days": 2.688194, + "weight": 0.00043271358403767666 + }, + { + "days": 2.689583, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.690278, + "weight": 0.00011801279564663909 + }, + { + "days": 2.69375, + "weight": 3.205285807686494e-05 + }, + { + "days": 2.694444, + "weight": 8.15890932865653e-05 + }, + { + "days": 2.698611, + "weight": 0.0001150988994578332 + }, + { + "days": 2.700694, + "weight": 3.6423702360073794e-05 + }, + { + "days": 2.713889, + "weight": 2.476811760485018e-05 + }, + { + "days": 2.729861, + "weight": 4.953623520970036e-05 + }, + { + "days": 2.731944, + "weight": 2.476811760485018e-05 + }, + { + "days": 2.739583, + "weight": 0.00018066156370596603 + }, + { + "days": 2.747222, + "weight": 0.000632315472970881 + }, + { + "days": 2.748611, + "weight": 6.410571615372988e-05 + }, + { + "days": 2.75, + "weight": 7.284740472014759e-05 + }, + { + "days": 2.750694, + "weight": 4.6622339020894455e-05 + }, + { + "days": 2.751389, + "weight": 4.807928711529741e-05 + }, + { + "days": 2.754861, + "weight": 9.761552232499777e-05 + }, + { + "days": 2.75625, + "weight": 0.00015880734228992173 + }, + { + "days": 2.757639, + "weight": 2.6225065699253133e-05 + }, + { + "days": 2.780556, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.793056, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.794444, + "weight": 4.516539092649151e-05 + }, + { + "days": 2.804861, + "weight": 5.9734871870521025e-05 + }, + { + "days": 2.805556, + "weight": 2.7682013793656084e-05 + }, + { + "days": 2.80625, + "weight": 0.001148075098389526 + }, + { + "days": 2.807639, + "weight": 7.721824900335645e-05 + }, + { + "days": 2.809028, + "weight": 2.6225065699253133e-05 + }, + { + "days": 2.813889, + "weight": 0.00033072721742947005 + }, + { + "days": 2.816667, + "weight": 7.721824900335645e-05 + }, + { + "days": 2.822222, + "weight": 4.079454664328265e-05 + }, + { + "days": 2.848611, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.849306, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.859028, + "weight": 2.7682013793656084e-05 + }, + { + "days": 2.864583, + "weight": 4.079454664328265e-05 + }, + { + "days": 2.867361, + "weight": 6.701961234253578e-05 + }, + { + "days": 2.868056, + "weight": 4.22514947376856e-05 + }, + { + "days": 2.870833, + "weight": 6.847656043693873e-05 + }, + { + "days": 2.871528, + "weight": 0.00015589344610111584 + }, + { + "days": 2.873611, + "weight": 4.079454664328265e-05 + }, + { + "days": 2.875, + "weight": 3.205285807686494e-05 + }, + { + "days": 2.875694, + "weight": 0.0002637076050869343 + }, + { + "days": 2.880556, + "weight": 0.005501436004465546 + }, + { + "days": 2.882639, + "weight": 0.00012821143230745975 + }, + { + "days": 2.890972, + "weight": 5.9734871870521025e-05 + }, + { + "days": 2.895833, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.913194, + "weight": 9.178772994738596e-05 + }, + { + "days": 2.914583, + "weight": 0.00014132396515708633 + }, + { + "days": 2.91875, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.919444, + "weight": 4.516539092649151e-05 + }, + { + "days": 2.922917, + "weight": 0.000151522601817907 + }, + { + "days": 2.925694, + "weight": 0.0002695353974645461 + }, + { + "days": 2.929167, + "weight": 0.00023165474701006934 + }, + { + "days": 2.934028, + "weight": 0.0002141713698772339 + }, + { + "days": 2.9375, + "weight": 5.827792377611807e-05 + }, + { + "days": 2.940972, + "weight": 0.000120926691835445 + }, + { + "days": 2.941667, + "weight": 0.0007809241785999821 + }, + { + "days": 2.942361, + "weight": 0.003467536464679025 + }, + { + "days": 2.95, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.954167, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.957639, + "weight": 7.139045662574464e-05 + }, + { + "days": 2.959028, + "weight": 3.788065045447675e-05 + }, + { + "days": 2.959722, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.960417, + "weight": 3.350980617126789e-05 + }, + { + "days": 2.965278, + "weight": 2.7682013793656084e-05 + }, + { + "days": 2.965972, + "weight": 3.788065045447675e-05 + }, + { + "days": 2.968056, + "weight": 0.00031032994410782873 + }, + { + "days": 2.969444, + "weight": 4.370844283208855e-05 + }, + { + "days": 2.971528, + "weight": 3.059590998246199e-05 + }, + { + "days": 2.972222, + "weight": 0.00037297871216715564 + }, + { + "days": 2.972917, + "weight": 3.788065045447675e-05 + }, + { + "days": 2.974306, + "weight": 9.761552232499777e-05 + }, + { + "days": 2.976389, + "weight": 2.9138961888059035e-05 + }, + { + "days": 2.977083, + "weight": 4.953623520970036e-05 + }, + { + "days": 2.978472, + "weight": 5.6820975681715117e-05 + }, + { + "days": 2.982639, + "weight": 5.6820975681715117e-05 + }, + { + "days": 2.984722, + "weight": 0.0002141713698772339 + }, + { + "days": 2.985417, + "weight": 3.496675426567084e-05 + }, + { + "days": 2.986806, + "weight": 4.079454664328265e-05 + }, + { + "days": 2.9875, + "weight": 0.0003977468297720058 + }, + { + "days": 2.990972, + "weight": 0.0006614544348589401 + }, + { + "days": 2.993056, + "weight": 0.0004997331963802125 + }, + { + "days": 2.995139, + "weight": 0.00046330949402013867 + }, + { + "days": 2.997917, + "weight": 0.00019668799274439848 + }, + { + "days": 3.0, + "weight": 0.0003744356602615586 + }, + { + "days": 3.008333, + "weight": 0.0019683368755383877 + }, + { + "days": 3.011111, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.015278, + "weight": 7.139045662574464e-05 + }, + { + "days": 3.017361, + "weight": 4.953623520970036e-05 + }, + { + "days": 3.030556, + "weight": 5.6820975681715117e-05 + }, + { + "days": 3.031944, + "weight": 4.22514947376856e-05 + }, + { + "days": 3.032639, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.033333, + "weight": 0.00014715175753469813 + }, + { + "days": 3.036111, + "weight": 0.00011655584755223614 + }, + { + "days": 3.0375, + "weight": 4.6622339020894455e-05 + }, + { + "days": 3.038194, + "weight": 0.00025787981270932245 + }, + { + "days": 3.042361, + "weight": 0.0005332430025514804 + }, + { + "days": 3.04375, + "weight": 7.139045662574464e-05 + }, + { + "days": 3.048611, + "weight": 0.0003977468297720058 + }, + { + "days": 3.049306, + "weight": 0.00011364195136343023 + }, + { + "days": 3.050694, + "weight": 0.00027827708603096377 + }, + { + "days": 3.052083, + "weight": 2.6225065699253133e-05 + }, + { + "days": 3.055556, + "weight": 5.6820975681715117e-05 + }, + { + "days": 3.056944, + "weight": 7.57613009089535e-05 + }, + { + "days": 3.059722, + "weight": 0.000332184165523873 + }, + { + "days": 3.061111, + "weight": 0.00018794630417798077 + }, + { + "days": 3.090972, + "weight": 7.57613009089535e-05 + }, + { + "days": 3.095139, + "weight": 2.7682013793656084e-05 + }, + { + "days": 3.098611, + "weight": 3.059590998246199e-05 + }, + { + "days": 3.104167, + "weight": 4.22514947376856e-05 + }, + { + "days": 3.105556, + "weight": 3.496675426567084e-05 + }, + { + "days": 3.106944, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.107639, + "weight": 0.0001544364980067129 + }, + { + "days": 3.109028, + "weight": 4.807928711529741e-05 + }, + { + "days": 3.1125, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.114583, + "weight": 0.00012238363992984796 + }, + { + "days": 3.115278, + "weight": 5.827792377611807e-05 + }, + { + "days": 3.115972, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.116667, + "weight": 6.119181996492398e-05 + }, + { + "days": 3.123611, + "weight": 6.119181996492398e-05 + }, + { + "days": 3.138194, + "weight": 2.7682013793656084e-05 + }, + { + "days": 3.15, + "weight": 3.059590998246199e-05 + }, + { + "days": 3.156944, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.157639, + "weight": 3.788065045447675e-05 + }, + { + "days": 3.165278, + "weight": 6.701961234253578e-05 + }, + { + "days": 3.169444, + "weight": 4.516539092649151e-05 + }, + { + "days": 3.170139, + "weight": 3.788065045447675e-05 + }, + { + "days": 3.174306, + "weight": 3.059590998246199e-05 + }, + { + "days": 3.175, + "weight": 6.556266424813284e-05 + }, + { + "days": 3.177778, + "weight": 2.6225065699253133e-05 + }, + { + "days": 3.18125, + "weight": 0.00011218500326902729 + }, + { + "days": 3.213194, + "weight": 0.00015006565372350402 + }, + { + "days": 3.224306, + "weight": 8.887383375858005e-05 + }, + { + "days": 3.23125, + "weight": 4.953623520970036e-05 + }, + { + "days": 3.233333, + "weight": 3.059590998246199e-05 + }, + { + "days": 3.234028, + "weight": 4.370844283208855e-05 + }, + { + "days": 3.235417, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.236111, + "weight": 2.6225065699253133e-05 + }, + { + "days": 3.275, + "weight": 4.6622339020894455e-05 + }, + { + "days": 3.2875, + "weight": 8.887383375858005e-05 + }, + { + "days": 3.291667, + "weight": 2.7682013793656084e-05 + }, + { + "days": 3.293056, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.297917, + "weight": 2.6225065699253133e-05 + }, + { + "days": 3.300694, + "weight": 0.00010635721089141548 + }, + { + "days": 3.343056, + "weight": 5.3907079492909214e-05 + }, + { + "days": 3.347222, + "weight": 4.370844283208855e-05 + }, + { + "days": 3.349306, + "weight": 4.6622339020894455e-05 + }, + { + "days": 3.359028, + "weight": 3.059590998246199e-05 + }, + { + "days": 3.404167, + "weight": 3.350980617126789e-05 + }, + { + "days": 3.406944, + "weight": 2.7682013793656084e-05 + }, + { + "days": 3.409028, + "weight": 3.205285807686494e-05 + }, + { + "days": 3.417361, + "weight": 8.013214519216235e-05 + }, + { + "days": 3.419444, + "weight": 4.22514947376856e-05 + }, + { + "days": 3.478472, + "weight": 4.079454664328265e-05 + }, + { + "days": 3.479167, + "weight": 3.059590998246199e-05 + }, + { + "days": 3.485417, + "weight": 3.9337598548879697e-05 + }, + { + "days": 3.524306, + "weight": 2.7682013793656084e-05 + }, + { + "days": 3.533333, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.544444, + "weight": 4.370844283208855e-05 + }, + { + "days": 3.545139, + "weight": 7.721824900335645e-05 + }, + { + "days": 3.590278, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.611111, + "weight": 8.013214519216235e-05 + }, + { + "days": 3.660417, + "weight": 3.788065045447675e-05 + }, + { + "days": 3.670139, + "weight": 0.00014569480944029518 + }, + { + "days": 3.719444, + "weight": 4.807928711529741e-05 + }, + { + "days": 3.723611, + "weight": 2.7682013793656084e-05 + }, + { + "days": 3.727778, + "weight": 0.00017191987513954832 + }, + { + "days": 3.757639, + "weight": 3.350980617126789e-05 + }, + { + "days": 3.767361, + "weight": 3.788065045447675e-05 + }, + { + "days": 3.779167, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.782639, + "weight": 2.6225065699253133e-05 + }, + { + "days": 3.784722, + "weight": 3.788065045447675e-05 + }, + { + "days": 3.786111, + "weight": 0.0001937740965555926 + }, + { + "days": 3.798611, + "weight": 2.476811760485018e-05 + }, + { + "days": 3.804861, + "weight": 2.7682013793656084e-05 + }, + { + "days": 3.809028, + "weight": 3.9337598548879697e-05 + }, + { + "days": 3.822222, + "weight": 4.22514947376856e-05 + }, + { + "days": 3.831944, + "weight": 3.6423702360073794e-05 + }, + { + "days": 3.839583, + "weight": 2.7682013793656084e-05 + }, + { + "days": 3.840278, + "weight": 2.9138961888059035e-05 + }, + { + "days": 3.849306, + "weight": 0.0003773495564503645 + }, + { + "days": 3.857639, + "weight": 4.079454664328265e-05 + }, + { + "days": 3.86875, + "weight": 3.205285807686494e-05 + }, + { + "days": 3.884028, + "weight": 3.9337598548879697e-05 + }, + { + "days": 3.89375, + "weight": 4.079454664328265e-05 + }, + { + "days": 3.898611, + "weight": 0.0007386726838622965 + }, + { + "days": 3.899306, + "weight": 2.7682013793656084e-05 + }, + { + "days": 3.927083, + "weight": 7.284740472014759e-05 + }, + { + "days": 3.930556, + "weight": 5.536402758731217e-05 + }, + { + "days": 3.931944, + "weight": 0.00015589344610111584 + }, + { + "days": 3.932639, + "weight": 4.953623520970036e-05 + }, + { + "days": 3.943056, + "weight": 5.9734871870521025e-05 + }, + { + "days": 3.94375, + "weight": 3.205285807686494e-05 + }, + { + "days": 3.945833, + "weight": 5.536402758731217e-05 + }, + { + "days": 3.952083, + "weight": 3.059590998246199e-05 + }, + { + "days": 3.952778, + "weight": 9.907247041940072e-05 + }, + { + "days": 3.953472, + "weight": 4.516539092649151e-05 + }, + { + "days": 3.95625, + "weight": 3.496675426567084e-05 + }, + { + "days": 3.959722, + "weight": 0.00010781415898581843 + }, + { + "days": 3.961111, + "weight": 3.496675426567084e-05 + }, + { + "days": 3.963889, + "weight": 7.139045662574464e-05 + }, + { + "days": 3.965278, + "weight": 0.0003525814388455143 + }, + { + "days": 3.968056, + "weight": 3.205285807686494e-05 + }, + { + "days": 3.984028, + "weight": 3.205285807686494e-05 + }, + { + "days": 3.99375, + "weight": 0.00013986701706268336 + }, + { + "days": 3.995833, + "weight": 7.430435281455054e-05 + }, + { + "days": 3.996528, + "weight": 5.3907079492909214e-05 + }, + { + "days": 3.998611, + "weight": 7.139045662574464e-05 + }, + { + "days": 4.001389, + "weight": 0.00017337682323395127 + }, + { + "days": 4.002778, + "weight": 3.205285807686494e-05 + }, + { + "days": 4.004167, + "weight": 4.079454664328265e-05 + }, + { + "days": 4.00625, + "weight": 3.205285807686494e-05 + }, + { + "days": 4.007639, + "weight": 6.264876805932693e-05 + }, + { + "days": 4.008333, + "weight": 2.9138961888059035e-05 + }, + { + "days": 4.011806, + "weight": 0.00011655584755223614 + }, + { + "days": 4.015278, + "weight": 5.6820975681715117e-05 + }, + { + "days": 4.018056, + "weight": 4.953623520970036e-05 + }, + { + "days": 4.01875, + "weight": 5.6820975681715117e-05 + }, + { + "days": 4.019444, + "weight": 0.00019523104464999554 + }, + { + "days": 4.020833, + "weight": 0.00010052941851380368 + }, + { + "days": 4.021528, + "weight": 4.079454664328265e-05 + }, + { + "days": 4.025, + "weight": 0.00015589344610111584 + }, + { + "days": 4.029861, + "weight": 3.059590998246199e-05 + }, + { + "days": 4.035417, + "weight": 3.350980617126789e-05 + }, + { + "days": 4.047917, + "weight": 4.807928711529741e-05 + }, + { + "days": 4.054167, + "weight": 3.205285807686494e-05 + }, + { + "days": 4.059722, + "weight": 4.6622339020894455e-05 + }, + { + "days": 4.063194, + "weight": 7.284740472014759e-05 + }, + { + "days": 4.069444, + "weight": 2.476811760485018e-05 + }, + { + "days": 4.070139, + "weight": 5.9734871870521025e-05 + }, + { + "days": 4.074306, + "weight": 7.284740472014759e-05 + }, + { + "days": 4.078472, + "weight": 2.7682013793656084e-05 + }, + { + "days": 4.079861, + "weight": 3.496675426567084e-05 + }, + { + "days": 4.080556, + "weight": 2.7682013793656084e-05 + }, + { + "days": 4.082639, + "weight": 0.00010927110708022139 + }, + { + "days": 4.095833, + "weight": 2.6225065699253133e-05 + }, + { + "days": 4.11875, + "weight": 2.7682013793656084e-05 + }, + { + "days": 4.127083, + "weight": 5.2450131398506267e-05 + }, + { + "days": 4.129167, + "weight": 3.496675426567084e-05 + }, + { + "days": 4.131944, + "weight": 4.516539092649151e-05 + }, + { + "days": 4.1375, + "weight": 3.6423702360073794e-05 + }, + { + "days": 4.148611, + "weight": 7.57613009089535e-05 + }, + { + "days": 4.181944, + "weight": 4.22514947376856e-05 + }, + { + "days": 4.209028, + "weight": 2.7682013793656084e-05 + }, + { + "days": 4.211806, + "weight": 3.059590998246199e-05 + }, + { + "days": 4.23125, + "weight": 4.370844283208855e-05 + }, + { + "days": 4.268056, + "weight": 2.6225065699253133e-05 + }, + { + "days": 4.273611, + "weight": 2.9138961888059035e-05 + }, + { + "days": 4.305556, + "weight": 2.6225065699253133e-05 + }, + { + "days": 4.334722, + "weight": 8.595993756977416e-05 + }, + { + "days": 4.438889, + "weight": 3.205285807686494e-05 + }, + { + "days": 4.450694, + "weight": 2.476811760485018e-05 + }, + { + "days": 4.467361, + "weight": 2.9138961888059035e-05 + }, + { + "days": 4.619444, + "weight": 4.370844283208855e-05 + }, + { + "days": 4.731944, + "weight": 2.7682013793656084e-05 + }, + { + "days": 4.760417, + "weight": 2.9138961888059035e-05 + }, + { + "days": 4.770833, + "weight": 0.00017920461561156306 + }, + { + "days": 4.801389, + "weight": 2.7682013793656084e-05 + }, + { + "days": 4.827083, + "weight": 0.00013695312087387746 + }, + { + "days": 4.884722, + "weight": 0.00023311169510447228 + }, + { + "days": 4.886806, + "weight": 2.6225065699253133e-05 + }, + { + "days": 4.900694, + "weight": 3.205285807686494e-05 + }, + { + "days": 4.940278, + "weight": 2.6225065699253133e-05 + }, + { + "days": 4.942361, + "weight": 6.119181996492398e-05 + }, + { + "days": 4.95, + "weight": 8.887383375858005e-05 + }, + { + "days": 4.951389, + "weight": 0.00023311169510447228 + }, + { + "days": 4.952083, + "weight": 2.6225065699253133e-05 + }, + { + "days": 4.952778, + "weight": 2.6225065699253133e-05 + }, + { + "days": 4.963889, + "weight": 3.788065045447675e-05 + }, + { + "days": 4.970139, + "weight": 0.00015006565372350402 + }, + { + "days": 4.978472, + "weight": 3.205285807686494e-05 + }, + { + "days": 4.9875, + "weight": 5.9734871870521025e-05 + }, + { + "days": 4.99375, + "weight": 2.9138961888059035e-05 + }, + { + "days": 5.0, + "weight": 2.476811760485018e-05 + }, + { + "days": 5.00625, + "weight": 3.059590998246199e-05 + }, + { + "days": 5.007639, + "weight": 3.9337598548879697e-05 + }, + { + "days": 5.011806, + "weight": 3.9337598548879697e-05 + }, + { + "days": 5.016667, + "weight": 3.788065045447675e-05 + }, + { + "days": 5.018056, + "weight": 9.470162613619187e-05 + }, + { + "days": 5.019444, + "weight": 6.264876805932693e-05 + }, + { + "days": 5.027778, + "weight": 3.205285807686494e-05 + }, + { + "days": 5.03125, + "weight": 2.9138961888059035e-05 + }, + { + "days": 5.054861, + "weight": 6.556266424813284e-05 + }, + { + "days": 5.078472, + "weight": 4.516539092649151e-05 + }, + { + "days": 5.086111, + "weight": 3.205285807686494e-05 + }, + { + "days": 5.095833, + "weight": 3.496675426567084e-05 + }, + { + "days": 5.097917, + "weight": 2.6225065699253133e-05 + }, + { + "days": 5.103472, + "weight": 5.6820975681715117e-05 + }, + { + "days": 5.114583, + "weight": 3.059590998246199e-05 + }, + { + "days": 5.13125, + "weight": 2.9138961888059035e-05 + }, + { + "days": 5.771528, + "weight": 5.6820975681715117e-05 + }, + { + "days": 5.838194, + "weight": 8.45029894753712e-05 + }, + { + "days": 5.891667, + "weight": 3.496675426567084e-05 + }, + { + "days": 5.91875, + "weight": 0.00013403922468507157 + }, + { + "days": 5.936111, + "weight": 4.370844283208855e-05 + }, + { + "days": 5.947917, + "weight": 2.7682013793656084e-05 + }, + { + "days": 5.986111, + "weight": 6.993350853134168e-05 + }, + { + "days": 5.997222, + "weight": 3.496675426567084e-05 + }, + { + "days": 5.998611, + "weight": 2.9138961888059035e-05 + }, + { + "days": 6.002778, + "weight": 4.370844283208855e-05 + }, + { + "days": 6.00625, + "weight": 3.9337598548879697e-05 + }, + { + "days": 6.017361, + "weight": 3.6423702360073794e-05 + }, + { + "days": 6.04375, + "weight": 5.099318330410331e-05 + }, + { + "days": 6.124306, + "weight": 3.496675426567084e-05 + }, + { + "days": 6.765972, + "weight": 3.350980617126789e-05 + }, + { + "days": 6.844444, + "weight": 6.119181996492398e-05 + }, + { + "days": 6.902083, + "weight": 6.264876805932693e-05 + }, + { + "days": 6.947917, + "weight": 2.9138961888059035e-05 + }, + { + "days": 6.963194, + "weight": 6.556266424813284e-05 + }, + { + "days": 6.970833, + "weight": 3.205285807686494e-05 + }, + { + "days": 7.010417, + "weight": 5.099318330410331e-05 + }, + { + "days": 7.017361, + "weight": 4.22514947376856e-05 + }, + { + "days": 7.018056, + "weight": 2.6225065699253133e-05 + }, + { + "days": 7.051389, + "weight": 3.059590998246199e-05 + }, + { + "days": 7.965278, + "weight": 2.9138961888059035e-05 + }, + { + "days": 9.828472, + "weight": 8.013214519216235e-05 + }, + { + "days": 9.863889, + "weight": 4.22514947376856e-05 + }, + { + "days": 9.931944, + "weight": 8.304604138096825e-05 + }, + { + "days": 9.985417, + "weight": 3.6423702360073794e-05 + }, + { + "days": 10.048611, + "weight": 3.350980617126789e-05 + }, + { + "days": 10.845833, + "weight": 3.205285807686494e-05 + }, + { + "days": 10.914583, + "weight": 2.7682013793656084e-05 + }, + { + "days": 11.076389, + "weight": 5.6820975681715117e-05 + }, + { + "days": 11.150694, + "weight": 2.7682013793656084e-05 + }, + { + "days": 11.209722, + "weight": 4.370844283208855e-05 + }, + { + "days": 11.26875, + "weight": 3.9337598548879697e-05 + }, + { + "days": 11.844444, + "weight": 3.350980617126789e-05 + }, + { + "days": 11.904861, + "weight": 3.205285807686494e-05 + }, + { + "days": 12.906944, + "weight": 3.496675426567084e-05 + }, + { + "days": 16.880556, + "weight": 3.6423702360073794e-05 + }, + { + "days": 17.958333, + "weight": 4.370844283208855e-05 + }, + { + "days": 22.287131, + "weight": 0.008333333333333333 + }, + { + "days": 25.1029, + "weight": 0.008333333333333333 + }, + { + "days": 28.274415, + "weight": 0.008333333333333333 + }, + { + "days": 31.846621, + "weight": 0.008333333333333333 + }, + { + "days": 35.870141, + "weight": 0.008333333333333333 + }, + { + "days": 40.401996, + "weight": 0.008333333333333333 + }, + { + "days": 45.506408, + "weight": 0.008333333333333333 + }, + { + "days": 51.255714, + "weight": 0.008333333333333333 + }, + { + "days": 57.73139, + "weight": 0.008333333333333333 + }, + { + "days": 65.025208, + "weight": 0.008333333333333333 + }, + { + "days": 73.240531, + "weight": 0.008333333333333333 + }, + { + "days": 82.493782, + "weight": 0.008333333333333333 + }, + { + "days": 92.916094, + "weight": 0.008333333333333333 + }, + { + "days": 104.655168, + "weight": 0.008333333333333333 + }, + { + "days": 117.877362, + "weight": 0.008333333333333333 + }, + { + "days": 132.770057, + "weight": 0.008333333333333333 + }, + { + "days": 149.544303, + "weight": 0.008333333333333333 + }, + { + "days": 168.437818, + "weight": 0.008333333333333333 + }, + { + "days": 189.71835, + "weight": 0.008333333333333333 + }, + { + "days": 213.687476, + "weight": 0.008333333333333333 + }, + { + "days": 240.684877, + "weight": 0.008333333333333333 + }, + { + "days": 271.093144, + "weight": 0.008333333333333333 + }, + { + "days": 305.34321, + "weight": 0.008333333333333333 + }, + { + "days": 343.920448, + "weight": 0.008333333333333333 + }, + { + "new_client": true, + "weight": 0.01767569428129661 + } + ] +} \ No newline at end of file diff --git a/tools/DeltaIndexTestTool/dist-tail-30.json b/tools/DeltaIndexTestTool/dist-tail-30.json new file mode 100644 index 0000000000..a87a06a847 --- /dev/null +++ b/tools/DeltaIndexTestTool/dist-tail-30.json @@ -0,0 +1,7301 @@ +{ + "description": "Telemetry-derived from C:\\Users\\johnmcp\\Downloads\\export.csv: 784,419 download events, 1.55% net-new clients, observed ages 0-18.0 days, plus 30.0% reinstated stale tail out to 344 days", + "buckets": [ + { + "days": 0.0, + "weight": 3.5695228312872314e-05 + }, + { + "days": 0.000694, + "weight": 0.0003786243860329671 + }, + { + "days": 0.028472, + "weight": 0.0015246961807926888 + }, + { + "days": 0.042361, + "weight": 4.0794546643282644e-05 + }, + { + "days": 0.043056, + "weight": 5.736733121711622e-05 + }, + { + "days": 0.047222, + "weight": 0.005720160337137788 + }, + { + "days": 0.049306, + "weight": 3.5695228312872314e-05 + }, + { + "days": 0.050694, + "weight": 0.0024833680269098313 + }, + { + "days": 0.051389, + "weight": 0.006202045919361565 + }, + { + "days": 0.052083, + "weight": 2.5496591652051656e-05 + }, + { + "days": 0.054167, + "weight": 0.00358354595669586 + }, + { + "days": 0.054861, + "weight": 0.005611799822616569 + }, + { + "days": 0.055556, + "weight": 0.004612333429856144 + }, + { + "days": 0.056944, + "weight": 0.004902994574689533 + }, + { + "days": 0.057639, + "weight": 0.006765520594871907 + }, + { + "days": 0.058333, + "weight": 0.003002223667029082 + }, + { + "days": 0.059028, + "weight": 0.03097453436849495 + }, + { + "days": 0.059722, + "weight": 0.014137860071062642 + }, + { + "days": 0.060417, + "weight": 0.005934331707015022 + }, + { + "days": 0.061111, + "weight": 0.012726623723121583 + }, + { + "days": 0.061806, + "weight": 0.014233472289757836 + }, + { + "days": 0.0625, + "weight": 0.00265547002056118 + }, + { + "days": 0.063194, + "weight": 0.009821287104370297 + }, + { + "days": 0.063889, + "weight": 0.010939312648312762 + }, + { + "days": 0.064583, + "weight": 0.0025891788822658456 + }, + { + "days": 0.065278, + "weight": 0.005758405224615866 + }, + { + "days": 0.065972, + "weight": 0.007198962652956785 + }, + { + "days": 0.066667, + "weight": 0.011862289266117033 + }, + { + "days": 0.067361, + "weight": 0.003281411345619048 + }, + { + "days": 0.069444, + "weight": 0.004599585134030118 + }, + { + "days": 0.070833, + "weight": 0.0028097244000560924 + }, + { + "days": 0.072222, + "weight": 0.002972902586629223 + }, + { + "days": 0.072917, + "weight": 0.002608301326004884 + }, + { + "days": 0.074306, + "weight": 0.009084435605626004 + }, + { + "days": 0.076389, + "weight": 4.5893864973692975e-05 + }, + { + "days": 0.078472, + "weight": 0.0036804330049736564 + }, + { + "days": 0.084028, + "weight": 0.0024425734802665483 + }, + { + "days": 0.084722, + "weight": 0.00354402623963518 + }, + { + "days": 0.085417, + "weight": 0.0036090425483479116 + }, + { + "days": 0.0875, + "weight": 0.00016827750490354092 + }, + { + "days": 0.088889, + "weight": 0.0016980730040266402 + }, + { + "days": 0.102083, + "weight": 0.0009127779811434492 + }, + { + "days": 0.109722, + "weight": 0.0008745330936653717 + }, + { + "days": 0.110417, + "weight": 0.001408686688775854 + }, + { + "days": 0.113194, + "weight": 0.0035172548184005258 + }, + { + "days": 0.114583, + "weight": 0.0011294990101858883 + }, + { + "days": 0.115278, + "weight": 5.099318330410331e-05 + }, + { + "days": 0.115972, + "weight": 2.9321080399859404e-05 + }, + { + "days": 0.116667, + "weight": 0.0011447969651771192 + }, + { + "days": 0.117361, + "weight": 0.0022832197824412255 + }, + { + "days": 0.118056, + "weight": 0.0023482360911539573 + }, + { + "days": 0.11875, + "weight": 0.0015119478849666631 + }, + { + "days": 0.119444, + "weight": 0.0011906908301508123 + }, + { + "days": 0.120139, + "weight": 0.0015361696470361122 + }, + { + "days": 0.120833, + "weight": 0.0013398458913153144 + }, + { + "days": 0.121528, + "weight": 0.001358968335054353 + }, + { + "days": 0.122222, + "weight": 0.0012365846951245053 + }, + { + "days": 0.122917, + "weight": 0.0009892677560996042 + }, + { + "days": 0.123611, + "weight": 0.0004742366047281608 + }, + { + "days": 0.124306, + "weight": 0.001402312540862841 + }, + { + "days": 0.125, + "weight": 0.0005214052992844563 + }, + { + "days": 0.125694, + "weight": 0.0032648385610452146 + }, + { + "days": 0.126389, + "weight": 0.000601719562988419 + }, + { + "days": 0.127083, + "weight": 0.001958138238877567 + }, + { + "days": 0.127778, + "weight": 0.001973436193868798 + }, + { + "days": 0.130556, + "weight": 0.00021672102904243907 + }, + { + "days": 0.13125, + "weight": 0.0010262378139950792 + }, + { + "days": 0.131944, + "weight": 0.0005481767205191106 + }, + { + "days": 0.132639, + "weight": 0.0010593833831427462 + }, + { + "days": 0.133333, + "weight": 0.0021072933000420694 + }, + { + "days": 0.134028, + "weight": 0.0016368811840617161 + }, + { + "days": 0.134722, + "weight": 0.0005048325147106227 + }, + { + "days": 0.136111, + "weight": 0.00104153576898631 + }, + { + "days": 0.1375, + "weight": 0.00043471688766748073 + }, + { + "days": 0.138194, + "weight": 0.0003837237043633774 + }, + { + "days": 0.14375, + "weight": 0.0003046842702420173 + }, + { + "days": 0.144444, + "weight": 0.0008987548557348208 + }, + { + "days": 0.147917, + "weight": 0.0008260895695264736 + }, + { + "days": 0.148611, + "weight": 0.00018612511905997708 + }, + { + "days": 0.149306, + "weight": 0.0006998814408488179 + }, + { + "days": 0.150694, + "weight": 0.0009573970165345396 + }, + { + "days": 0.161111, + "weight": 0.0005405277430234951 + }, + { + "days": 0.164583, + "weight": 2.677142123465424e-05 + }, + { + "days": 0.168056, + "weight": 0.0005328787655278796 + }, + { + "days": 0.169444, + "weight": 0.001197064978063825 + }, + { + "days": 0.172222, + "weight": 0.0005328787655278796 + }, + { + "days": 0.172917, + "weight": 4.716869455629556e-05 + }, + { + "days": 0.173611, + "weight": 0.0007916691707962038 + }, + { + "days": 0.175, + "weight": 0.0004028461481024161 + }, + { + "days": 0.175694, + "weight": 0.000980343949021386 + }, + { + "days": 0.176389, + "weight": 0.00045511416098912205 + }, + { + "days": 0.177083, + "weight": 0.0007827453637179858 + }, + { + "days": 0.177778, + "weight": 0.0014431070875061236 + }, + { + "days": 0.178472, + "weight": 0.000470412115980353 + }, + { + "days": 0.179167, + "weight": 0.001708271640687461 + }, + { + "days": 0.179861, + "weight": 0.0007865698524657936 + }, + { + "days": 0.18125, + "weight": 0.0018612511905997707 + }, + { + "days": 0.181944, + "weight": 0.0010122146885864507 + }, + { + "days": 0.182639, + "weight": 0.00040922029601542904 + }, + { + "days": 0.183333, + "weight": 0.0004079454664328265 + }, + { + "days": 0.184028, + "weight": 0.0007738215566397678 + }, + { + "days": 0.184722, + "weight": 0.0010517344056471308 + }, + { + "days": 0.185417, + "weight": 0.0005379780838582899 + }, + { + "days": 0.186111, + "weight": 0.001931366817642913 + }, + { + "days": 0.186806, + "weight": 0.00023201898403367005 + }, + { + "days": 0.1875, + "weight": 0.00033273052105927406 + }, + { + "days": 0.188889, + "weight": 0.0006119181996492397 + }, + { + "days": 0.190972, + "weight": 0.0003747998972851593 + }, + { + "days": 0.192361, + "weight": 0.001093803781873016 + }, + { + "days": 0.194444, + "weight": 0.0004946338780498021 + }, + { + "days": 0.195139, + "weight": 0.002144263357937544 + }, + { + "days": 0.196528, + "weight": 0.0008847317303261925 + }, + { + "days": 0.197222, + "weight": 0.0003097835885724276 + }, + { + "days": 0.197917, + "weight": 0.00020142307405120806 + }, + { + "days": 0.2, + "weight": 0.00027663801942476047 + }, + { + "days": 0.202083, + "weight": 0.0011282241806032857 + }, + { + "days": 0.204167, + "weight": 0.0004729617751455582 + }, + { + "days": 0.208333, + "weight": 0.0005073821738758279 + }, + { + "days": 0.210417, + "weight": 0.0006182923475622526 + }, + { + "days": 0.211111, + "weight": 0.0003786243860329671 + }, + { + "days": 0.2125, + "weight": 0.00046021347931953237 + }, + { + "days": 0.218056, + "weight": 0.00015807886824272026 + }, + { + "days": 0.222917, + "weight": 0.00025496591652051655 + }, + { + "days": 0.227083, + "weight": 0.00024476727985969587 + }, + { + "days": 0.229167, + "weight": 8.286392286916788e-05 + }, + { + "days": 0.23125, + "weight": 0.0005609250163451364 + }, + { + "days": 0.232639, + "weight": 9.306255952998854e-05 + }, + { + "days": 0.233333, + "weight": 7.648977495615496e-05 + }, + { + "days": 0.234722, + "weight": 8.413875245177046e-05 + }, + { + "days": 0.235417, + "weight": 8.158909328656529e-05 + }, + { + "days": 0.236111, + "weight": 0.00021544619945983648 + }, + { + "days": 0.2375, + "weight": 0.00021289654029463132 + }, + { + "days": 0.238194, + "weight": 8.796324119957821e-05 + }, + { + "days": 0.238889, + "weight": 0.00033273052105927406 + }, + { + "days": 0.239583, + "weight": 0.000177201311981759 + }, + { + "days": 0.240278, + "weight": 0.0003250815435636586 + }, + { + "days": 0.240972, + "weight": 0.00034547881688529994 + }, + { + "days": 0.242361, + "weight": 0.00028428699692037597 + }, + { + "days": 0.243056, + "weight": 0.0006310406433882784 + }, + { + "days": 0.24375, + "weight": 0.00043471688766748073 + }, + { + "days": 0.244444, + "weight": 8.923807078218079e-05 + }, + { + "days": 0.245833, + "weight": 0.00018867477822518224 + }, + { + "days": 0.247917, + "weight": 0.00023839313194668296 + }, + { + "days": 0.248611, + "weight": 6.374147913012914e-05 + }, + { + "days": 0.249306, + "weight": 0.00019504892613819515 + }, + { + "days": 0.251389, + "weight": 9.943670744300145e-05 + }, + { + "days": 0.252083, + "weight": 0.0009229766178042699 + }, + { + "days": 0.252778, + "weight": 0.00021544619945983648 + }, + { + "days": 0.253472, + "weight": 0.00036460126062433867 + }, + { + "days": 0.254167, + "weight": 0.0001835754598947719 + }, + { + "days": 0.254861, + "weight": 8.668841161697562e-05 + }, + { + "days": 0.255556, + "weight": 0.00010453602577341178 + }, + { + "days": 0.25625, + "weight": 9.433738911259112e-05 + }, + { + "days": 0.258333, + "weight": 0.00020397273321641325 + }, + { + "days": 0.259028, + "weight": 6.884079746053947e-05 + }, + { + "days": 0.259722, + "weight": 6.119181996492397e-05 + }, + { + "days": 0.261111, + "weight": 0.0001975985853034003 + }, + { + "days": 0.265278, + "weight": 0.00011728432159943762 + }, + { + "days": 0.268056, + "weight": 0.0001555292090775151 + }, + { + "days": 0.271528, + "weight": 0.00034930330563310766 + }, + { + "days": 0.274306, + "weight": 8.286392286916788e-05 + }, + { + "days": 0.275, + "weight": 8.923807078218079e-05 + }, + { + "days": 0.276389, + "weight": 0.00010071153702560403 + }, + { + "days": 0.279861, + "weight": 0.000177201311981759 + }, + { + "days": 0.281944, + "weight": 0.00012493329909505312 + }, + { + "days": 0.284028, + "weight": 3.314556914766715e-05 + }, + { + "days": 0.286806, + "weight": 0.00010198636660820662 + }, + { + "days": 0.288194, + "weight": 6.884079746053947e-05 + }, + { + "days": 0.290278, + "weight": 0.00018612511905997708 + }, + { + "days": 0.292361, + "weight": 8.541358203437304e-05 + }, + { + "days": 0.293056, + "weight": 0.00011091017368642469 + }, + { + "days": 0.295139, + "weight": 0.00010198636660820662 + }, + { + "days": 0.297222, + "weight": 6.119181996492397e-05 + }, + { + "days": 0.297917, + "weight": 0.0003072339294072224 + }, + { + "days": 0.299306, + "weight": 0.0003467536464679025 + }, + { + "days": 0.3, + "weight": 0.00018867477822518224 + }, + { + "days": 0.300694, + "weight": 0.00026006523485092687 + }, + { + "days": 0.301389, + "weight": 0.00016062852740792542 + }, + { + "days": 0.303472, + "weight": 5.736733121711622e-05 + }, + { + "days": 0.304167, + "weight": 0.00026898904192914497 + }, + { + "days": 0.305556, + "weight": 0.00027663801942476047 + }, + { + "days": 0.30625, + "weight": 0.0001542543794949125 + }, + { + "days": 0.306944, + "weight": 0.00012238363992984793 + }, + { + "days": 0.308333, + "weight": 0.00021162171071202872 + }, + { + "days": 0.309722, + "weight": 0.0008911058782392053 + }, + { + "days": 0.310417, + "weight": 0.00010071153702560403 + }, + { + "days": 0.311111, + "weight": 0.0001415060836688867 + }, + { + "days": 0.311806, + "weight": 0.00033910466897228703 + }, + { + "days": 0.3125, + "weight": 0.0002141713698772339 + }, + { + "days": 0.313194, + "weight": 0.00031233324773763274 + }, + { + "days": 0.313889, + "weight": 9.68870482777963e-05 + }, + { + "days": 0.314583, + "weight": 0.00013130744700806603 + }, + { + "days": 0.315972, + "weight": 9.816187786039887e-05 + }, + { + "days": 0.316667, + "weight": 7.648977495615496e-05 + }, + { + "days": 0.317361, + "weight": 9.433738911259112e-05 + }, + { + "days": 0.31875, + "weight": 3.69700578954749e-05 + }, + { + "days": 0.320139, + "weight": 0.00026261489401613206 + }, + { + "days": 0.320833, + "weight": 7.011562704314205e-05 + }, + { + "days": 0.322222, + "weight": 0.00014405574283409185 + }, + { + "days": 0.323611, + "weight": 3.824488747807748e-05 + }, + { + "days": 0.326389, + "weight": 0.0001325822765906686 + }, + { + "days": 0.327778, + "weight": 0.00010708568493861695 + }, + { + "days": 0.33125, + "weight": 0.00015680403866011767 + }, + { + "days": 0.332639, + "weight": 0.0052816189607225 + }, + { + "days": 0.335417, + "weight": 0.000146605401999297 + }, + { + "days": 0.338194, + "weight": 7.39401157909498e-05 + }, + { + "days": 0.340278, + "weight": 0.00012110881034724535 + }, + { + "days": 0.340972, + "weight": 0.00026388972359873465 + }, + { + "days": 0.343056, + "weight": 0.00010198636660820662 + }, + { + "days": 0.34375, + "weight": 0.00012875778784286084 + }, + { + "days": 0.345139, + "weight": 2.4221762069449073e-05 + }, + { + "days": 0.345833, + "weight": 0.00012238363992984793 + }, + { + "days": 0.349306, + "weight": 8.286392286916788e-05 + }, + { + "days": 0.353472, + "weight": 0.00010453602577341178 + }, + { + "days": 0.354861, + "weight": 9.56122186951937e-05 + }, + { + "days": 0.356944, + "weight": 5.991699038232139e-05 + }, + { + "days": 0.357639, + "weight": 0.00011983398076464277 + }, + { + "days": 0.358333, + "weight": 5.226801288670589e-05 + }, + { + "days": 0.359028, + "weight": 0.00046913728639775046 + }, + { + "days": 0.359722, + "weight": 0.00019249926697299 + }, + { + "days": 0.360417, + "weight": 0.00015297954991230992 + }, + { + "days": 0.361111, + "weight": 8.923807078218079e-05 + }, + { + "days": 0.361806, + "weight": 3.824488747807748e-05 + }, + { + "days": 0.3625, + "weight": 0.00023966796152928555 + }, + { + "days": 0.363194, + "weight": 0.00017975097114696418 + }, + { + "days": 0.363889, + "weight": 7.139045662574463e-05 + }, + { + "days": 0.365972, + "weight": 0.00015042989074710476 + }, + { + "days": 0.366667, + "weight": 7.266528620834722e-05 + }, + { + "days": 0.367361, + "weight": 5.354284246930848e-05 + }, + { + "days": 0.36875, + "weight": 8.923807078218079e-05 + }, + { + "days": 0.369444, + "weight": 0.00011091017368642469 + }, + { + "days": 0.370139, + "weight": 0.00021162171071202872 + }, + { + "days": 0.371528, + "weight": 0.0003314556914766715 + }, + { + "days": 0.372222, + "weight": 0.00012620812867765568 + }, + { + "days": 0.372917, + "weight": 0.00017592648239915642 + }, + { + "days": 0.373611, + "weight": 0.00015680403866011767 + }, + { + "days": 0.375, + "weight": 0.0012824785600981982 + }, + { + "days": 0.376389, + "weight": 0.0003021346110768121 + }, + { + "days": 0.377083, + "weight": 0.00012110881034724535 + }, + { + "days": 0.377778, + "weight": 0.00014278091325148925 + }, + { + "days": 0.378472, + "weight": 0.00012620812867765568 + }, + { + "days": 0.379861, + "weight": 0.00010198636660820662 + }, + { + "days": 0.38125, + "weight": 0.00010708568493861695 + }, + { + "days": 0.3875, + "weight": 0.00010071153702560403 + }, + { + "days": 0.388889, + "weight": 3.9519717060680065e-05 + }, + { + "days": 0.390278, + "weight": 9.051290036478337e-05 + }, + { + "days": 0.392361, + "weight": 9.051290036478337e-05 + }, + { + "days": 0.397222, + "weight": 0.00016445301615573317 + }, + { + "days": 0.398611, + "weight": 0.00018612511905997708 + }, + { + "days": 0.399306, + "weight": 0.00010198636660820662 + }, + { + "days": 0.400694, + "weight": 0.00016700267532093833 + }, + { + "days": 0.404167, + "weight": 0.0001032611961908092 + }, + { + "days": 0.406944, + "weight": 0.0014533057241669443 + }, + { + "days": 0.407639, + "weight": 0.00012748295826025828 + }, + { + "days": 0.409028, + "weight": 9.816187786039887e-05 + }, + { + "days": 0.4125, + "weight": 0.0002065223923816184 + }, + { + "days": 0.414583, + "weight": 5.609250163451364e-05 + }, + { + "days": 0.415972, + "weight": 6.884079746053947e-05 + }, + { + "days": 0.418056, + "weight": 0.00021289654029463132 + }, + { + "days": 0.41875, + "weight": 8.413875245177046e-05 + }, + { + "days": 0.419444, + "weight": 0.00016700267532093833 + }, + { + "days": 0.420139, + "weight": 0.0002422176206944907 + }, + { + "days": 0.421528, + "weight": 0.0003314556914766715 + }, + { + "days": 0.422222, + "weight": 0.00016572784573833576 + }, + { + "days": 0.422917, + "weight": 6.756596787793689e-05 + }, + { + "days": 0.424306, + "weight": 0.00017082716406874608 + }, + { + "days": 0.425, + "weight": 0.00024094279111188814 + }, + { + "days": 0.426389, + "weight": 9.56122186951937e-05 + }, + { + "days": 0.427083, + "weight": 0.00016700267532093833 + }, + { + "days": 0.427778, + "weight": 0.00021289654029463132 + }, + { + "days": 0.429861, + "weight": 0.0012314853767940949 + }, + { + "days": 0.43125, + "weight": 3.824488747807748e-05 + }, + { + "days": 0.431944, + "weight": 0.0001389564245036815 + }, + { + "days": 0.432639, + "weight": 0.00016955233448614351 + }, + { + "days": 0.434028, + "weight": 0.0001491550611645022 + }, + { + "days": 0.435417, + "weight": 0.0001032611961908092 + }, + { + "days": 0.436806, + "weight": 3.4420398730269734e-05 + }, + { + "days": 0.4375, + "weight": 0.0005787726305015726 + }, + { + "days": 0.438194, + "weight": 0.00017210199365134867 + }, + { + "days": 0.438889, + "weight": 0.00021162171071202872 + }, + { + "days": 0.440278, + "weight": 0.00011473466243423244 + }, + { + "days": 0.442361, + "weight": 0.00013130744700806603 + }, + { + "days": 0.443056, + "weight": 0.00011345983285162986 + }, + { + "days": 0.446528, + "weight": 9.178772994738595e-05 + }, + { + "days": 0.447222, + "weight": 0.00026134006443352946 + }, + { + "days": 0.447917, + "weight": 0.00015935369782532285 + }, + { + "days": 0.449306, + "weight": 0.00011983398076464277 + }, + { + "days": 0.453472, + "weight": 0.0002906611448333889 + }, + { + "days": 0.457639, + "weight": 0.0002804625081725682 + }, + { + "days": 0.458333, + "weight": 0.00033910466897228703 + }, + { + "days": 0.459028, + "weight": 6.119181996492397e-05 + }, + { + "days": 0.460417, + "weight": 0.00017082716406874608 + }, + { + "days": 0.4625, + "weight": 0.0007597984312311393 + }, + { + "days": 0.466667, + "weight": 0.0015438186245317277 + }, + { + "days": 0.468056, + "weight": 0.00033400535064187666 + }, + { + "days": 0.470833, + "weight": 3.5695228312872314e-05 + }, + { + "days": 0.472222, + "weight": 0.00015042989074710476 + }, + { + "days": 0.472917, + "weight": 0.00013640676533847635 + }, + { + "days": 0.473611, + "weight": 0.0003747998972851593 + }, + { + "days": 0.474306, + "weight": 0.0004066706368502239 + }, + { + "days": 0.475, + "weight": 0.00022182034737284939 + }, + { + "days": 0.477083, + "weight": 0.0007368514987442928 + }, + { + "days": 0.477778, + "weight": 0.00024731693902490105 + }, + { + "days": 0.479167, + "weight": 0.00035057813521571025 + }, + { + "days": 0.480556, + "weight": 0.0003837237043633774 + }, + { + "days": 0.48125, + "weight": 0.00042196859184145486 + }, + { + "days": 0.483333, + "weight": 0.00033018086189406893 + }, + { + "days": 0.484028, + "weight": 0.001023688154829874 + }, + { + "days": 0.484722, + "weight": 0.0002294693248684649 + }, + { + "days": 0.485417, + "weight": 0.00022182034737284939 + }, + { + "days": 0.486806, + "weight": 0.0009242514473868725 + }, + { + "days": 0.4875, + "weight": 0.00041559444392844195 + }, + { + "days": 0.488194, + "weight": 0.00026388972359873465 + }, + { + "days": 0.488889, + "weight": 0.00023201898403367005 + }, + { + "days": 0.490278, + "weight": 0.0006068188813188294 + }, + { + "days": 0.490972, + "weight": 0.000177201311981759 + }, + { + "days": 0.491667, + "weight": 0.0003097835885724276 + }, + { + "days": 0.492361, + "weight": 0.0001975985853034003 + }, + { + "days": 0.493056, + "weight": 0.0006068188813188294 + }, + { + "days": 0.49375, + "weight": 0.0004908093893019943 + }, + { + "days": 0.494444, + "weight": 0.00269498973762186 + }, + { + "days": 0.495833, + "weight": 0.0002970352927464018 + }, + { + "days": 0.498611, + "weight": 0.0005736733121711622 + }, + { + "days": 0.499306, + "weight": 0.00020269790363381066 + }, + { + "days": 0.500694, + "weight": 0.00022564483612065714 + }, + { + "days": 0.502083, + "weight": 0.00024731693902490105 + }, + { + "days": 0.50625, + "weight": 0.00028428699692037597 + }, + { + "days": 0.506944, + "weight": 0.0005825971192493804 + }, + { + "days": 0.509028, + "weight": 0.00028428699692037597 + }, + { + "days": 0.511111, + "weight": 0.0008490365020133201 + }, + { + "days": 0.5125, + "weight": 0.00010708568493861695 + }, + { + "days": 0.517361, + "weight": 0.0003467536464679025 + }, + { + "days": 0.51875, + "weight": 0.00020397273321641325 + }, + { + "days": 0.521528, + "weight": 0.0004206937622588523 + }, + { + "days": 0.522917, + "weight": 0.00020779722196422097 + }, + { + "days": 0.524306, + "weight": 0.0003008597814942095 + }, + { + "days": 0.525, + "weight": 0.0001402312540862841 + }, + { + "days": 0.526389, + "weight": 0.00019504892613819515 + }, + { + "days": 0.527083, + "weight": 0.0001415060836688867 + }, + { + "days": 0.532639, + "weight": 0.00031360807732023533 + }, + { + "days": 0.533333, + "weight": 0.0005634746755103416 + }, + { + "days": 0.535417, + "weight": 0.0003467536464679025 + }, + { + "days": 0.536111, + "weight": 0.0002371183023640804 + }, + { + "days": 0.536806, + "weight": 0.0010491847464819256 + }, + { + "days": 0.538194, + "weight": 6.62911382953343e-05 + }, + { + "days": 0.539583, + "weight": 0.00044109103558049364 + }, + { + "days": 0.540278, + "weight": 0.0004206937622588523 + }, + { + "days": 0.543056, + "weight": 0.00035695228312872316 + }, + { + "days": 0.544444, + "weight": 0.0006603617237881379 + }, + { + "days": 0.545833, + "weight": 0.0002855618265029785 + }, + { + "days": 0.546528, + "weight": 0.0005494515501017132 + }, + { + "days": 0.547222, + "weight": 0.00024859176860750364 + }, + { + "days": 0.548611, + "weight": 0.00015680403866011767 + }, + { + "days": 0.549306, + "weight": 0.00023456864319887523 + }, + { + "days": 0.55, + "weight": 0.0011167507143598624 + }, + { + "days": 0.550694, + "weight": 0.0007419508170747032 + }, + { + "days": 0.551389, + "weight": 0.00032125705481585084 + }, + { + "days": 0.552083, + "weight": 7.521494537355238e-05 + }, + { + "days": 0.552778, + "weight": 0.00015170472032970735 + }, + { + "days": 0.553472, + "weight": 0.0003939223410241981 + }, + { + "days": 0.554167, + "weight": 4.716869455629556e-05 + }, + { + "days": 0.554861, + "weight": 0.00020269790363381066 + }, + { + "days": 0.555556, + "weight": 0.00017975097114696418 + }, + { + "days": 0.556944, + "weight": 0.00013513193575587378 + }, + { + "days": 0.557639, + "weight": 0.00022691966570325973 + }, + { + "days": 0.558333, + "weight": 0.0002511414277727088 + }, + { + "days": 0.559722, + "weight": 0.000308508758989825 + }, + { + "days": 0.560417, + "weight": 8.031426370396271e-05 + }, + { + "days": 0.568056, + "weight": 0.00010581085535601436 + }, + { + "days": 0.570139, + "weight": 0.00041049512559803163 + }, + { + "days": 0.570833, + "weight": 6.62911382953343e-05 + }, + { + "days": 0.572917, + "weight": 0.00011473466243423244 + }, + { + "days": 0.575, + "weight": 0.00012875778784286084 + }, + { + "days": 0.58125, + "weight": 0.00018102580072956674 + }, + { + "days": 0.584722, + "weight": 0.0004882597301367892 + }, + { + "days": 0.5875, + "weight": 2.804625081725682e-05 + }, + { + "days": 0.590278, + "weight": 5.609250163451364e-05 + }, + { + "days": 0.591667, + "weight": 0.0002830121673377734 + }, + { + "days": 0.592361, + "weight": 0.000161903356990528 + }, + { + "days": 0.593056, + "weight": 6.501630871273172e-05 + }, + { + "days": 0.59375, + "weight": 8.413875245177046e-05 + }, + { + "days": 0.595833, + "weight": 9.051290036478337e-05 + }, + { + "days": 0.597222, + "weight": 0.0001912244373903874 + }, + { + "days": 0.597917, + "weight": 0.0008477616724307175 + }, + { + "days": 0.598611, + "weight": 0.0002995849519116069 + }, + { + "days": 0.599306, + "weight": 6.756596787793689e-05 + }, + { + "days": 0.6, + "weight": 9.68870482777963e-05 + }, + { + "days": 0.601389, + "weight": 0.00021162171071202872 + }, + { + "days": 0.602083, + "weight": 4.334420580848781e-05 + }, + { + "days": 0.602778, + "weight": 0.00015297954991230992 + }, + { + "days": 0.603472, + "weight": 0.00015935369782532285 + }, + { + "days": 0.605556, + "weight": 0.00033910466897228703 + }, + { + "days": 0.606944, + "weight": 0.00012748295826025828 + }, + { + "days": 0.607639, + "weight": 0.00011091017368642469 + }, + { + "days": 0.609028, + "weight": 0.00011600949201683502 + }, + { + "days": 0.609722, + "weight": 0.00018230063031216933 + }, + { + "days": 0.610417, + "weight": 9.68870482777963e-05 + }, + { + "days": 0.611111, + "weight": 0.001325822765906686 + }, + { + "days": 0.611806, + "weight": 0.00023201898403367005 + }, + { + "days": 0.6125, + "weight": 4.9718353721500726e-05 + }, + { + "days": 0.613194, + "weight": 3.824488747807748e-05 + }, + { + "days": 0.613889, + "weight": 0.00022054551779024682 + }, + { + "days": 0.614583, + "weight": 0.0002001482444686055 + }, + { + "days": 0.615278, + "weight": 5.609250163451364e-05 + }, + { + "days": 0.615972, + "weight": 5.609250163451364e-05 + }, + { + "days": 0.616667, + "weight": 0.00026134006443352946 + }, + { + "days": 0.617361, + "weight": 0.00016062852740792542 + }, + { + "days": 0.61875, + "weight": 0.0001389564245036815 + }, + { + "days": 0.619444, + "weight": 6.374147913012914e-05 + }, + { + "days": 0.620139, + "weight": 0.00023074415445106748 + }, + { + "days": 0.622222, + "weight": 5.991699038232139e-05 + }, + { + "days": 0.623611, + "weight": 7.266528620834722e-05 + }, + { + "days": 0.624306, + "weight": 8.413875245177046e-05 + }, + { + "days": 0.628472, + "weight": 5.991699038232139e-05 + }, + { + "days": 0.631944, + "weight": 0.0001912244373903874 + }, + { + "days": 0.632639, + "weight": 6.119181996492397e-05 + }, + { + "days": 0.634028, + "weight": 0.0003187073956506457 + }, + { + "days": 0.642361, + "weight": 8.031426370396271e-05 + }, + { + "days": 0.644444, + "weight": 0.0003875481931111852 + }, + { + "days": 0.648611, + "weight": 8.286392286916788e-05 + }, + { + "days": 0.652778, + "weight": 0.0011702935568291709 + }, + { + "days": 0.654167, + "weight": 0.00032763120272886375 + }, + { + "days": 0.654861, + "weight": 9.943670744300145e-05 + }, + { + "days": 0.655556, + "weight": 0.0002830121673377734 + }, + { + "days": 0.65625, + "weight": 4.206937622588523e-05 + }, + { + "days": 0.656944, + "weight": 5.736733121711622e-05 + }, + { + "days": 0.657639, + "weight": 3.69700578954749e-05 + }, + { + "days": 0.658333, + "weight": 0.00015170472032970735 + }, + { + "days": 0.659028, + "weight": 5.226801288670589e-05 + }, + { + "days": 0.660417, + "weight": 0.00016827750490354092 + }, + { + "days": 0.661806, + "weight": 0.00012620812867765568 + }, + { + "days": 0.6625, + "weight": 5.354284246930848e-05 + }, + { + "days": 0.663194, + "weight": 7.776460453875755e-05 + }, + { + "days": 0.663889, + "weight": 4.206937622588523e-05 + }, + { + "days": 0.664583, + "weight": 0.0006246664954752655 + }, + { + "days": 0.665278, + "weight": 9.051290036478337e-05 + }, + { + "days": 0.665972, + "weight": 0.00012620812867765568 + }, + { + "days": 0.666667, + "weight": 8.158909328656529e-05 + }, + { + "days": 0.667361, + "weight": 0.00011091017368642469 + }, + { + "days": 0.668056, + "weight": 2.9321080399859404e-05 + }, + { + "days": 0.66875, + "weight": 0.0007330270099964851 + }, + { + "days": 0.669444, + "weight": 0.00015170472032970735 + }, + { + "days": 0.670139, + "weight": 0.001185591511820402 + }, + { + "days": 0.670833, + "weight": 0.00011091017368642469 + }, + { + "days": 0.671528, + "weight": 9.943670744300145e-05 + }, + { + "days": 0.672917, + "weight": 0.00017337682323395124 + }, + { + "days": 0.673611, + "weight": 0.00015042989074710476 + }, + { + "days": 0.674306, + "weight": 0.0001185591511820402 + }, + { + "days": 0.675, + "weight": 5.481767205191106e-05 + }, + { + "days": 0.675694, + "weight": 0.00017592648239915642 + }, + { + "days": 0.676389, + "weight": 0.00013640676533847635 + }, + { + "days": 0.677083, + "weight": 0.00016317818657313058 + }, + { + "days": 0.678472, + "weight": 7.011562704314205e-05 + }, + { + "days": 0.68125, + "weight": 4.206937622588523e-05 + }, + { + "days": 0.681944, + "weight": 0.0001975985853034003 + }, + { + "days": 0.682639, + "weight": 8.923807078218079e-05 + }, + { + "days": 0.683333, + "weight": 0.0002740883602595553 + }, + { + "days": 0.684028, + "weight": 0.0003161577364854405 + }, + { + "days": 0.6875, + "weight": 9.433738911259112e-05 + }, + { + "days": 0.688889, + "weight": 0.00012620812867765568 + }, + { + "days": 0.690278, + "weight": 3.9519717060680065e-05 + }, + { + "days": 0.69375, + "weight": 7.266528620834722e-05 + }, + { + "days": 0.695833, + "weight": 4.844352413889815e-05 + }, + { + "days": 0.698611, + "weight": 0.0006335903025534836 + }, + { + "days": 0.70625, + "weight": 2.2946932486846487e-05 + }, + { + "days": 0.707639, + "weight": 8.031426370396271e-05 + }, + { + "days": 0.713889, + "weight": 0.00030595909982461983 + }, + { + "days": 0.716667, + "weight": 0.00011345983285162986 + }, + { + "days": 0.71875, + "weight": 0.000592795755910201 + }, + { + "days": 0.719444, + "weight": 0.0005456270613539054 + }, + { + "days": 0.720139, + "weight": 0.0010759561677165799 + }, + { + "days": 0.720833, + "weight": 0.0002740883602595553 + }, + { + "days": 0.721528, + "weight": 0.00014405574283409185 + }, + { + "days": 0.722222, + "weight": 6.884079746053947e-05 + }, + { + "days": 0.723611, + "weight": 0.00015042989074710476 + }, + { + "days": 0.725, + "weight": 6.374147913012914e-05 + }, + { + "days": 0.725694, + "weight": 8.541358203437304e-05 + }, + { + "days": 0.726389, + "weight": 0.0002651645531813372 + }, + { + "days": 0.727083, + "weight": 8.031426370396271e-05 + }, + { + "days": 0.727778, + "weight": 0.00010963534410382211 + }, + { + "days": 0.728472, + "weight": 0.00016827750490354092 + }, + { + "days": 0.729167, + "weight": 0.00021799585862504163 + }, + { + "days": 0.729861, + "weight": 7.903943412136013e-05 + }, + { + "days": 0.730556, + "weight": 0.00096249633486495 + }, + { + "days": 0.73125, + "weight": 7.011562704314205e-05 + }, + { + "days": 0.731944, + "weight": 5.354284246930848e-05 + }, + { + "days": 0.732639, + "weight": 9.816187786039887e-05 + }, + { + "days": 0.733333, + "weight": 5.736733121711622e-05 + }, + { + "days": 0.734028, + "weight": 0.0001835754598947719 + }, + { + "days": 0.734722, + "weight": 6.501630871273172e-05 + }, + { + "days": 0.735417, + "weight": 0.00024986659819010624 + }, + { + "days": 0.736806, + "weight": 7.39401157909498e-05 + }, + { + "days": 0.7375, + "weight": 0.00010708568493861695 + }, + { + "days": 0.738194, + "weight": 0.000501008025962815 + }, + { + "days": 0.738889, + "weight": 0.0001937740965555926 + }, + { + "days": 0.740278, + "weight": 0.0001032611961908092 + }, + { + "days": 0.740972, + "weight": 4.4619035391090396e-05 + }, + { + "days": 0.741667, + "weight": 7.903943412136013e-05 + }, + { + "days": 0.743056, + "weight": 3.059590998246198e-05 + }, + { + "days": 0.744444, + "weight": 7.521494537355238e-05 + }, + { + "days": 0.746528, + "weight": 7.266528620834722e-05 + }, + { + "days": 0.747222, + "weight": 0.0003939223410241981 + }, + { + "days": 0.748611, + "weight": 9.56122186951937e-05 + }, + { + "days": 0.749306, + "weight": 0.00010581085535601436 + }, + { + "days": 0.75, + "weight": 4.4619035391090396e-05 + }, + { + "days": 0.750694, + "weight": 8.541358203437304e-05 + }, + { + "days": 0.7625, + "weight": 5.099318330410331e-05 + }, + { + "days": 0.776389, + "weight": 0.00016700267532093833 + }, + { + "days": 0.777083, + "weight": 4.5893864973692975e-05 + }, + { + "days": 0.777778, + "weight": 4.206937622588523e-05 + }, + { + "days": 0.778472, + "weight": 0.0003709754085373516 + }, + { + "days": 0.779167, + "weight": 0.0001415060836688867 + }, + { + "days": 0.779861, + "weight": 0.0017656389719045771 + }, + { + "days": 0.780556, + "weight": 0.00032890603231146634 + }, + { + "days": 0.78125, + "weight": 8.031426370396271e-05 + }, + { + "days": 0.781944, + "weight": 7.139045662574463e-05 + }, + { + "days": 0.782639, + "weight": 0.0002434924502770933 + }, + { + "days": 0.783333, + "weight": 6.884079746053947e-05 + }, + { + "days": 0.784028, + "weight": 0.00015042989074710476 + }, + { + "days": 0.785417, + "weight": 0.0003314556914766715 + }, + { + "days": 0.786806, + "weight": 0.00022182034737284939 + }, + { + "days": 0.7875, + "weight": 0.0020295286955033117 + }, + { + "days": 0.788194, + "weight": 0.00043216722850227554 + }, + { + "days": 0.788889, + "weight": 3.314556914766715e-05 + }, + { + "days": 0.789583, + "weight": 0.0005723984825885596 + }, + { + "days": 0.790972, + "weight": 5.354284246930848e-05 + }, + { + "days": 0.791667, + "weight": 0.00025496591652051655 + }, + { + "days": 0.792361, + "weight": 0.00022437000653805457 + }, + { + "days": 0.793056, + "weight": 7.139045662574463e-05 + }, + { + "days": 0.79375, + "weight": 9.943670744300145e-05 + }, + { + "days": 0.795833, + "weight": 0.00031998222523324824 + }, + { + "days": 0.796528, + "weight": 0.00011345983285162986 + }, + { + "days": 0.797917, + "weight": 0.0001338571061732712 + }, + { + "days": 0.798611, + "weight": 0.00027663801942476047 + }, + { + "days": 0.799306, + "weight": 7.648977495615496e-05 + }, + { + "days": 0.8, + "weight": 0.00014278091325148925 + }, + { + "days": 0.800694, + "weight": 4.844352413889815e-05 + }, + { + "days": 0.801389, + "weight": 0.0002881114856681837 + }, + { + "days": 0.802778, + "weight": 6.756596787793689e-05 + }, + { + "days": 0.804167, + "weight": 8.541358203437304e-05 + }, + { + "days": 0.804861, + "weight": 0.00026134006443352946 + }, + { + "days": 0.805556, + "weight": 6.374147913012914e-05 + }, + { + "days": 0.80625, + "weight": 4.5893864973692975e-05 + }, + { + "days": 0.808333, + "weight": 7.139045662574463e-05 + }, + { + "days": 0.809028, + "weight": 7.266528620834722e-05 + }, + { + "days": 0.810417, + "weight": 0.00027663801942476047 + }, + { + "days": 0.811111, + "weight": 0.00021162171071202872 + }, + { + "days": 0.827083, + "weight": 7.776460453875755e-05 + }, + { + "days": 0.832639, + "weight": 0.0001325822765906686 + }, + { + "days": 0.836806, + "weight": 5.864216079971881e-05 + }, + { + "days": 0.838194, + "weight": 0.0002651645531813372 + }, + { + "days": 0.838889, + "weight": 0.0006833086562749843 + }, + { + "days": 0.839583, + "weight": 0.00037225023811995417 + }, + { + "days": 0.840278, + "weight": 7.139045662574463e-05 + }, + { + "days": 0.840972, + "weight": 4.0794546643282644e-05 + }, + { + "days": 0.841667, + "weight": 0.00017465165281655383 + }, + { + "days": 0.842361, + "weight": 8.158909328656529e-05 + }, + { + "days": 0.84375, + "weight": 0.00026261489401613206 + }, + { + "days": 0.844444, + "weight": 0.00011600949201683502 + }, + { + "days": 0.845833, + "weight": 0.00020142307405120806 + }, + { + "days": 0.846528, + "weight": 0.000546901890936508 + }, + { + "days": 0.847222, + "weight": 0.0007266528620834721 + }, + { + "days": 0.849306, + "weight": 0.002183783074998224 + }, + { + "days": 0.85, + "weight": 0.0001338571061732712 + }, + { + "days": 0.850694, + "weight": 0.0003786243860329671 + }, + { + "days": 0.851389, + "weight": 0.000177201311981759 + }, + { + "days": 0.852778, + "weight": 5.864216079971881e-05 + }, + { + "days": 0.854167, + "weight": 6.756596787793689e-05 + }, + { + "days": 0.854861, + "weight": 0.00042579308058926263 + }, + { + "days": 0.855556, + "weight": 0.0001848502894773745 + }, + { + "days": 0.856944, + "weight": 0.00022437000653805457 + }, + { + "days": 0.857639, + "weight": 8.413875245177046e-05 + }, + { + "days": 0.858333, + "weight": 7.648977495615496e-05 + }, + { + "days": 0.860417, + "weight": 0.0001835754598947719 + }, + { + "days": 0.861111, + "weight": 0.0003633264310417361 + }, + { + "days": 0.861806, + "weight": 0.00010836051452121953 + }, + { + "days": 0.863889, + "weight": 0.0003008597814942095 + }, + { + "days": 0.864583, + "weight": 0.000146605401999297 + }, + { + "days": 0.865278, + "weight": 0.0002371183023640804 + }, + { + "days": 0.865972, + "weight": 0.00013768159492107894 + }, + { + "days": 0.866667, + "weight": 9.943670744300145e-05 + }, + { + "days": 0.868056, + "weight": 0.00012875778784286084 + }, + { + "days": 0.86875, + "weight": 0.0002358434727814778 + }, + { + "days": 0.870139, + "weight": 0.00021162171071202872 + }, + { + "days": 0.870833, + "weight": 0.00018994960780778484 + }, + { + "days": 0.872222, + "weight": 0.00018230063031216933 + }, + { + "days": 0.882639, + "weight": 8.541358203437304e-05 + }, + { + "days": 0.890278, + "weight": 4.4619035391090396e-05 + }, + { + "days": 0.891667, + "weight": 0.00025369108693791396 + }, + { + "days": 0.896528, + "weight": 0.000369700578954749 + }, + { + "days": 0.897917, + "weight": 8.413875245177046e-05 + }, + { + "days": 0.898611, + "weight": 0.00033400535064187666 + }, + { + "days": 0.901389, + "weight": 0.0001937740965555926 + }, + { + "days": 0.902083, + "weight": 8.668841161697562e-05 + }, + { + "days": 0.902778, + "weight": 0.00017847614156436158 + }, + { + "days": 0.903472, + "weight": 0.00016955233448614351 + }, + { + "days": 0.904861, + "weight": 0.00026261489401613206 + }, + { + "days": 0.905556, + "weight": 0.0028326713325429386 + }, + { + "days": 0.90625, + "weight": 0.00044619035391090396 + }, + { + "days": 0.906944, + "weight": 0.00046021347931953237 + }, + { + "days": 0.907639, + "weight": 0.00031998222523324824 + }, + { + "days": 0.908333, + "weight": 0.0003607767718765309 + }, + { + "days": 0.909722, + "weight": 5.864216079971881e-05 + }, + { + "days": 0.913194, + "weight": 0.00014533057241669444 + }, + { + "days": 0.913889, + "weight": 0.00017465165281655383 + }, + { + "days": 0.914583, + "weight": 0.0003110584181550302 + }, + { + "days": 0.915278, + "weight": 8.796324119957821e-05 + }, + { + "days": 0.915972, + "weight": 0.0006450637687969069 + }, + { + "days": 0.916667, + "weight": 0.00031233324773763274 + }, + { + "days": 0.917361, + "weight": 2.677142123465424e-05 + }, + { + "days": 0.918056, + "weight": 0.00015935369782532285 + }, + { + "days": 0.921528, + "weight": 0.008364156891455546 + }, + { + "days": 0.922222, + "weight": 0.00016700267532093833 + }, + { + "days": 0.922917, + "weight": 0.0006743848491967663 + }, + { + "days": 0.923611, + "weight": 0.00023329381361627264 + }, + { + "days": 0.924306, + "weight": 0.0019453899430515412 + }, + { + "days": 0.925, + "weight": 0.00015297954991230992 + }, + { + "days": 0.925694, + "weight": 0.0003607767718765309 + }, + { + "days": 0.926389, + "weight": 7.776460453875755e-05 + }, + { + "days": 0.927083, + "weight": 0.0001491550611645022 + }, + { + "days": 0.930556, + "weight": 0.0004206937622588523 + }, + { + "days": 0.93125, + "weight": 0.00024859176860750364 + }, + { + "days": 0.931944, + "weight": 0.00013768159492107894 + }, + { + "days": 0.932639, + "weight": 0.00021672102904243907 + }, + { + "days": 0.933333, + "weight": 0.00024859176860750364 + }, + { + "days": 0.94375, + "weight": 0.0003314556914766715 + }, + { + "days": 0.948611, + "weight": 0.00016955233448614351 + }, + { + "days": 0.950694, + "weight": 0.0002294693248684649 + }, + { + "days": 0.951389, + "weight": 0.0009102283219782441 + }, + { + "days": 0.955556, + "weight": 0.0005800474600841752 + }, + { + "days": 0.95625, + "weight": 0.002067773582981389 + }, + { + "days": 0.956944, + "weight": 0.0007113549070922411 + }, + { + "days": 0.957639, + "weight": 0.0002434924502770933 + }, + { + "days": 0.958333, + "weight": 0.00016572784573833576 + }, + { + "days": 0.959028, + "weight": 0.0004206937622588523 + }, + { + "days": 0.959722, + "weight": 4.844352413889815e-05 + }, + { + "days": 0.960417, + "weight": 0.002545834676457358 + }, + { + "days": 0.961111, + "weight": 0.0012735547530199802 + }, + { + "days": 0.961806, + "weight": 0.00016700267532093833 + }, + { + "days": 0.9625, + "weight": 0.0009777942898561809 + }, + { + "days": 0.964583, + "weight": 0.000794218829961409 + }, + { + "days": 0.965278, + "weight": 0.0003671509197895438 + }, + { + "days": 0.965972, + "weight": 0.0011103765664468497 + }, + { + "days": 0.968056, + "weight": 0.0006386896208838939 + }, + { + "days": 0.96875, + "weight": 0.005013904748375958 + }, + { + "days": 0.970139, + "weight": 0.00034802847605050507 + }, + { + "days": 0.972222, + "weight": 0.0012633561163591595 + }, + { + "days": 0.972917, + "weight": 0.006152327565640064 + }, + { + "days": 0.973611, + "weight": 0.0010912541227078108 + }, + { + "days": 0.974306, + "weight": 0.001133323498933696 + }, + { + "days": 0.975, + "weight": 0.0005443522317713028 + }, + { + "days": 0.975694, + "weight": 0.00017847614156436158 + }, + { + "days": 0.976389, + "weight": 0.0006705603604489585 + }, + { + "days": 0.977083, + "weight": 0.0028097244000560924 + }, + { + "days": 0.977778, + "weight": 3.314556914766715e-05 + }, + { + "days": 0.979167, + "weight": 0.001203439125976838 + }, + { + "days": 0.979861, + "weight": 0.0006144678588144448 + }, + { + "days": 0.980556, + "weight": 0.0009153276403086544 + }, + { + "days": 0.98125, + "weight": 0.001225111228881082 + }, + { + "days": 0.981944, + "weight": 0.0027753040013258224 + }, + { + "days": 0.982639, + "weight": 0.0036549364133216045 + }, + { + "days": 0.983333, + "weight": 0.0008515861611785253 + }, + { + "days": 0.984722, + "weight": 0.0002919359744159914 + }, + { + "days": 0.985417, + "weight": 0.0006692855308663559 + }, + { + "days": 0.986111, + "weight": 0.006009546652388575 + }, + { + "days": 0.986806, + "weight": 0.00023074415445106748 + }, + { + "days": 0.988194, + "weight": 0.007553365276920303 + }, + { + "days": 0.988889, + "weight": 0.0007929440003788064 + }, + { + "days": 0.989583, + "weight": 0.0002995849519116069 + }, + { + "days": 0.990278, + "weight": 0.0013143492996632628 + }, + { + "days": 0.990972, + "weight": 0.00242855035485792 + }, + { + "days": 0.991667, + "weight": 2.9321080399859404e-05 + }, + { + "days": 0.995139, + "weight": 0.0008056922962048323 + }, + { + "days": 0.995833, + "weight": 0.0001032611961908092 + }, + { + "days": 0.998611, + "weight": 0.0003977468297720058 + }, + { + "days": 1.002778, + "weight": 0.0002995849519116069 + }, + { + "days": 1.007639, + "weight": 0.0010810554860469902 + }, + { + "days": 1.008333, + "weight": 3.314556914766715e-05 + }, + { + "days": 1.009722, + "weight": 0.0024451231394317535 + }, + { + "days": 1.011111, + "weight": 0.0002983101223290044 + }, + { + "days": 1.014583, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.015278, + "weight": 0.0017452416985829357 + }, + { + "days": 1.016667, + "weight": 0.0017146457886004739 + }, + { + "days": 1.017361, + "weight": 0.0013156241292458654 + }, + { + "days": 1.018056, + "weight": 0.015911148020462836 + }, + { + "days": 1.01875, + "weight": 0.001472428167905983 + }, + { + "days": 1.020139, + "weight": 0.00043981620599789105 + }, + { + "days": 1.021528, + "weight": 0.0001338571061732712 + }, + { + "days": 1.022917, + "weight": 0.0011065520776990419 + }, + { + "days": 1.023611, + "weight": 0.0001542543794949125 + }, + { + "days": 1.024306, + "weight": 0.0002970352927464018 + }, + { + "days": 1.025, + "weight": 0.00019632375572079775 + }, + { + "days": 1.027083, + "weight": 0.0005813222896667778 + }, + { + "days": 1.027778, + "weight": 0.0036115922075131168 + }, + { + "days": 1.028472, + "weight": 7.648977495615496e-05 + }, + { + "days": 1.029861, + "weight": 0.0015132227145492657 + }, + { + "days": 1.03125, + "weight": 0.00016827750490354092 + }, + { + "days": 1.031944, + "weight": 0.0017120961294352687 + }, + { + "days": 1.032639, + "weight": 0.0015565669203577534 + }, + { + "days": 1.033333, + "weight": 0.00018739994864257965 + }, + { + "days": 1.034028, + "weight": 0.0009369997432128983 + }, + { + "days": 1.034722, + "weight": 0.0041699675646930485 + }, + { + "days": 1.035417, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.036111, + "weight": 0.0009905425856822068 + }, + { + "days": 1.036806, + "weight": 0.00020269790363381066 + }, + { + "days": 1.038889, + "weight": 0.0006450637687969069 + }, + { + "days": 1.039583, + "weight": 0.0008235399103612684 + }, + { + "days": 1.040972, + "weight": 0.00042324342142405745 + }, + { + "days": 1.041667, + "weight": 0.00045511416098912205 + }, + { + "days": 1.042361, + "weight": 0.0010287874731602844 + }, + { + "days": 1.043056, + "weight": 0.0016394308432269213 + }, + { + "days": 1.04375, + "weight": 0.0009637711644475526 + }, + { + "days": 1.044444, + "weight": 0.00036460126062433867 + }, + { + "days": 1.045139, + "weight": 0.0011868663414030046 + }, + { + "days": 1.045833, + "weight": 0.0004806107526411737 + }, + { + "days": 1.046528, + "weight": 0.0005915209263275984 + }, + { + "days": 1.048611, + "weight": 0.0004640379680673401 + }, + { + "days": 1.05, + "weight": 0.0005022828555454176 + }, + { + "days": 1.050694, + "weight": 0.0003709754085373516 + }, + { + "days": 1.051389, + "weight": 0.01807453382213942 + }, + { + "days": 1.052083, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.052778, + "weight": 3.9519717060680065e-05 + }, + { + "days": 1.054861, + "weight": 0.002360984386979983 + }, + { + "days": 1.056944, + "weight": 0.00026898904192914497 + }, + { + "days": 1.058333, + "weight": 0.0004079454664328265 + }, + { + "days": 1.064583, + "weight": 0.00020269790363381066 + }, + { + "days": 1.065972, + "weight": 5.609250163451364e-05 + }, + { + "days": 1.069444, + "weight": 0.0003620516014591335 + }, + { + "days": 1.073611, + "weight": 0.00030595909982461983 + }, + { + "days": 1.074306, + "weight": 0.00010071153702560403 + }, + { + "days": 1.076389, + "weight": 0.0002728135306769527 + }, + { + "days": 1.077083, + "weight": 0.00024094279111188814 + }, + { + "days": 1.077778, + "weight": 0.00015297954991230992 + }, + { + "days": 1.079167, + "weight": 7.011562704314205e-05 + }, + { + "days": 1.079861, + "weight": 0.00013003261742546344 + }, + { + "days": 1.08125, + "weight": 8.541358203437304e-05 + }, + { + "days": 1.082639, + "weight": 0.0004806107526411737 + }, + { + "days": 1.084028, + "weight": 5.609250163451364e-05 + }, + { + "days": 1.084722, + "weight": 0.0012225615697158767 + }, + { + "days": 1.086111, + "weight": 0.000177201311981759 + }, + { + "days": 1.0875, + "weight": 5.991699038232139e-05 + }, + { + "days": 1.089583, + "weight": 0.0002817373377551708 + }, + { + "days": 1.090972, + "weight": 0.00016317818657313058 + }, + { + "days": 1.091667, + "weight": 0.0006769345083619715 + }, + { + "days": 1.092361, + "weight": 0.0010249629844124766 + }, + { + "days": 1.09375, + "weight": 0.0004283427397544678 + }, + { + "days": 1.094444, + "weight": 0.0001491550611645022 + }, + { + "days": 1.095139, + "weight": 0.00021672102904243907 + }, + { + "days": 1.097917, + "weight": 0.0011983398076464277 + }, + { + "days": 1.098611, + "weight": 8.158909328656529e-05 + }, + { + "days": 1.1, + "weight": 0.0001338571061732712 + }, + { + "days": 1.100694, + "weight": 0.0003824488747807748 + }, + { + "days": 1.101389, + "weight": 0.00036587609020694126 + }, + { + "days": 1.102083, + "weight": 0.00031233324773763274 + }, + { + "days": 1.104167, + "weight": 0.0001835754598947719 + }, + { + "days": 1.104861, + "weight": 0.00013130744700806603 + }, + { + "days": 1.105556, + "weight": 6.501630871273172e-05 + }, + { + "days": 1.10625, + "weight": 0.00018612511905997708 + }, + { + "days": 1.108333, + "weight": 0.00018867477822518224 + }, + { + "days": 1.109028, + "weight": 6.119181996492397e-05 + }, + { + "days": 1.109722, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.110417, + "weight": 0.00020269790363381066 + }, + { + "days": 1.111806, + "weight": 0.00016572784573833576 + }, + { + "days": 1.1125, + "weight": 0.0002001482444686055 + }, + { + "days": 1.113889, + "weight": 0.00012110881034724535 + }, + { + "days": 1.115972, + "weight": 6.501630871273172e-05 + }, + { + "days": 1.116667, + "weight": 0.00011600949201683502 + }, + { + "days": 1.118056, + "weight": 0.0004028461481024161 + }, + { + "days": 1.120139, + "weight": 0.00037352506770255676 + }, + { + "days": 1.120833, + "weight": 0.008078595064952567 + }, + { + "days": 1.131944, + "weight": 5.864216079971881e-05 + }, + { + "days": 1.136111, + "weight": 0.00014533057241669444 + }, + { + "days": 1.136806, + "weight": 0.00017847614156436158 + }, + { + "days": 1.1375, + "weight": 0.0001415060836688867 + }, + { + "days": 1.138889, + "weight": 0.00034930330563310766 + }, + { + "days": 1.140278, + "weight": 3.9519717060680065e-05 + }, + { + "days": 1.141667, + "weight": 7.648977495615496e-05 + }, + { + "days": 1.145833, + "weight": 0.00010453602577341178 + }, + { + "days": 1.146528, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.147222, + "weight": 8.413875245177046e-05 + }, + { + "days": 1.147917, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.148611, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.149306, + "weight": 8.668841161697562e-05 + }, + { + "days": 1.15, + "weight": 0.00010071153702560403 + }, + { + "days": 1.150694, + "weight": 0.00025624074610311915 + }, + { + "days": 1.151389, + "weight": 0.00044746518349350655 + }, + { + "days": 1.152083, + "weight": 9.178772994738595e-05 + }, + { + "days": 1.153472, + "weight": 0.00011218500326902729 + }, + { + "days": 1.154861, + "weight": 9.943670744300145e-05 + }, + { + "days": 1.155556, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.156944, + "weight": 3.69700578954749e-05 + }, + { + "days": 1.157639, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.159028, + "weight": 0.00034037949855488957 + }, + { + "days": 1.159722, + "weight": 0.00034165432813749216 + }, + { + "days": 1.160417, + "weight": 0.0010670323606383617 + }, + { + "days": 1.161111, + "weight": 0.00020397273321641325 + }, + { + "days": 1.161806, + "weight": 5.226801288670589e-05 + }, + { + "days": 1.163194, + "weight": 0.00010963534410382211 + }, + { + "days": 1.163889, + "weight": 0.0002587904052683243 + }, + { + "days": 1.165278, + "weight": 0.00023329381361627264 + }, + { + "days": 1.165972, + "weight": 3.824488747807748e-05 + }, + { + "days": 1.166667, + "weight": 3.5695228312872314e-05 + }, + { + "days": 1.167361, + "weight": 3.5695228312872314e-05 + }, + { + "days": 1.168056, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.170139, + "weight": 5.991699038232139e-05 + }, + { + "days": 1.170833, + "weight": 9.178772994738595e-05 + }, + { + "days": 1.171528, + "weight": 0.00011600949201683502 + }, + { + "days": 1.172917, + "weight": 4.716869455629556e-05 + }, + { + "days": 1.174306, + "weight": 0.00020524756279901581 + }, + { + "days": 1.176389, + "weight": 0.00016572784573833576 + }, + { + "days": 1.177083, + "weight": 0.0001415060836688867 + }, + { + "days": 1.178472, + "weight": 8.031426370396271e-05 + }, + { + "days": 1.181944, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.184722, + "weight": 4.0794546643282644e-05 + }, + { + "days": 1.1875, + "weight": 0.00013640676533847635 + }, + { + "days": 1.189583, + "weight": 2.1672102904243905e-05 + }, + { + "days": 1.191667, + "weight": 7.011562704314205e-05 + }, + { + "days": 1.193056, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.19375, + "weight": 0.00012620812867765568 + }, + { + "days": 1.194444, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.198611, + "weight": 7.011562704314205e-05 + }, + { + "days": 1.199306, + "weight": 0.0013908390746194177 + }, + { + "days": 1.200694, + "weight": 3.187073956506457e-05 + }, + { + "days": 1.202778, + "weight": 7.776460453875755e-05 + }, + { + "days": 1.203472, + "weight": 4.716869455629556e-05 + }, + { + "days": 1.204861, + "weight": 7.266528620834722e-05 + }, + { + "days": 1.205556, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.206944, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.209028, + "weight": 0.00013640676533847635 + }, + { + "days": 1.209722, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.210417, + "weight": 0.0003620516014591335 + }, + { + "days": 1.211111, + "weight": 3.824488747807748e-05 + }, + { + "days": 1.2125, + "weight": 0.0001975985853034003 + }, + { + "days": 1.213194, + "weight": 8.158909328656529e-05 + }, + { + "days": 1.213889, + "weight": 0.0001491550611645022 + }, + { + "days": 1.214583, + "weight": 0.00010581085535601436 + }, + { + "days": 1.215278, + "weight": 9.433738911259112e-05 + }, + { + "days": 1.215972, + "weight": 3.69700578954749e-05 + }, + { + "days": 1.218056, + "weight": 0.00010836051452121953 + }, + { + "days": 1.219444, + "weight": 0.00010071153702560403 + }, + { + "days": 1.220139, + "weight": 3.314556914766715e-05 + }, + { + "days": 1.220833, + "weight": 0.00061701751797965 + }, + { + "days": 1.222222, + "weight": 0.00026388972359873465 + }, + { + "days": 1.222917, + "weight": 5.226801288670589e-05 + }, + { + "days": 1.224306, + "weight": 0.00010071153702560403 + }, + { + "days": 1.225694, + "weight": 0.0007585236016485368 + }, + { + "days": 1.226389, + "weight": 8.668841161697562e-05 + }, + { + "days": 1.228472, + "weight": 0.00010581085535601436 + }, + { + "days": 1.229167, + "weight": 3.9519717060680065e-05 + }, + { + "days": 1.229861, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.231944, + "weight": 0.0001185591511820402 + }, + { + "days": 1.232639, + "weight": 7.39401157909498e-05 + }, + { + "days": 1.233333, + "weight": 9.56122186951937e-05 + }, + { + "days": 1.234028, + "weight": 0.00012238363992984793 + }, + { + "days": 1.234722, + "weight": 6.246664954752656e-05 + }, + { + "days": 1.2375, + "weight": 0.0003314556914766715 + }, + { + "days": 1.238194, + "weight": 6.501630871273172e-05 + }, + { + "days": 1.238889, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.240278, + "weight": 0.00012875778784286084 + }, + { + "days": 1.246528, + "weight": 0.001365342482967366 + }, + { + "days": 1.251389, + "weight": 5.481767205191106e-05 + }, + { + "days": 1.254167, + "weight": 0.00013513193575587378 + }, + { + "days": 1.25625, + "weight": 8.923807078218079e-05 + }, + { + "days": 1.258333, + "weight": 9.306255952998854e-05 + }, + { + "days": 1.261806, + "weight": 0.0002001482444686055 + }, + { + "days": 1.263889, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.264583, + "weight": 2.5496591652051656e-05 + }, + { + "days": 1.265972, + "weight": 8.541358203437304e-05 + }, + { + "days": 1.267361, + "weight": 0.00018994960780778484 + }, + { + "days": 1.268056, + "weight": 0.00015042989074710476 + }, + { + "days": 1.26875, + "weight": 9.816187786039887e-05 + }, + { + "days": 1.270139, + "weight": 7.011562704314205e-05 + }, + { + "days": 1.272222, + "weight": 8.031426370396271e-05 + }, + { + "days": 1.273611, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.274306, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.275, + "weight": 0.00010836051452121953 + }, + { + "days": 1.276389, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.277083, + "weight": 0.00011983398076464277 + }, + { + "days": 1.278472, + "weight": 7.39401157909498e-05 + }, + { + "days": 1.279167, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.279861, + "weight": 3.5695228312872314e-05 + }, + { + "days": 1.281944, + "weight": 3.187073956506457e-05 + }, + { + "days": 1.282639, + "weight": 0.00024604210944229846 + }, + { + "days": 1.283333, + "weight": 8.541358203437304e-05 + }, + { + "days": 1.284028, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.284722, + "weight": 0.0002970352927464018 + }, + { + "days": 1.285417, + "weight": 0.00011091017368642469 + }, + { + "days": 1.286111, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.288194, + "weight": 0.00025624074610311915 + }, + { + "days": 1.290278, + "weight": 3.5695228312872314e-05 + }, + { + "days": 1.29375, + "weight": 0.00010708568493861695 + }, + { + "days": 1.294444, + "weight": 9.178772994738595e-05 + }, + { + "days": 1.295139, + "weight": 3.314556914766715e-05 + }, + { + "days": 1.295833, + "weight": 4.0794546643282644e-05 + }, + { + "days": 1.296528, + "weight": 0.0001325822765906686 + }, + { + "days": 1.297917, + "weight": 9.178772994738595e-05 + }, + { + "days": 1.298611, + "weight": 0.0004117699551806342 + }, + { + "days": 1.299306, + "weight": 0.0001185591511820402 + }, + { + "days": 1.303472, + "weight": 0.0001988734148860029 + }, + { + "days": 1.306944, + "weight": 8.286392286916788e-05 + }, + { + "days": 1.313194, + "weight": 7.521494537355238e-05 + }, + { + "days": 1.315972, + "weight": 5.226801288670589e-05 + }, + { + "days": 1.316667, + "weight": 3.9519717060680065e-05 + }, + { + "days": 1.317361, + "weight": 3.69700578954749e-05 + }, + { + "days": 1.322917, + "weight": 7.521494537355238e-05 + }, + { + "days": 1.325, + "weight": 5.609250163451364e-05 + }, + { + "days": 1.325694, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.326389, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.327083, + "weight": 7.266528620834722e-05 + }, + { + "days": 1.327778, + "weight": 2.4221762069449073e-05 + }, + { + "days": 1.328472, + "weight": 0.00012875778784286084 + }, + { + "days": 1.329167, + "weight": 3.9519717060680065e-05 + }, + { + "days": 1.329861, + "weight": 7.266528620834722e-05 + }, + { + "days": 1.330556, + "weight": 7.903943412136013e-05 + }, + { + "days": 1.33125, + "weight": 0.0005086570034584305 + }, + { + "days": 1.332639, + "weight": 8.668841161697562e-05 + }, + { + "days": 1.333333, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.334722, + "weight": 9.56122186951937e-05 + }, + { + "days": 1.336806, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.338194, + "weight": 8.668841161697562e-05 + }, + { + "days": 1.338889, + "weight": 7.011562704314205e-05 + }, + { + "days": 1.339583, + "weight": 6.62911382953343e-05 + }, + { + "days": 1.340278, + "weight": 3.9519717060680065e-05 + }, + { + "days": 1.340972, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.342361, + "weight": 0.0002830121673377734 + }, + { + "days": 1.343056, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.344444, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.345139, + "weight": 9.178772994738595e-05 + }, + { + "days": 1.346528, + "weight": 0.00015297954991230992 + }, + { + "days": 1.348611, + "weight": 0.00016700267532093833 + }, + { + "days": 1.35, + "weight": 0.000146605401999297 + }, + { + "days": 1.352083, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.352778, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.353472, + "weight": 7.521494537355238e-05 + }, + { + "days": 1.354167, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.354861, + "weight": 4.844352413889815e-05 + }, + { + "days": 1.355556, + "weight": 7.776460453875755e-05 + }, + { + "days": 1.357639, + "weight": 0.00011600949201683502 + }, + { + "days": 1.360417, + "weight": 0.0004066706368502239 + }, + { + "days": 1.361111, + "weight": 8.286392286916788e-05 + }, + { + "days": 1.363194, + "weight": 8.796324119957821e-05 + }, + { + "days": 1.372222, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.372917, + "weight": 9.051290036478337e-05 + }, + { + "days": 1.377083, + "weight": 4.0794546643282644e-05 + }, + { + "days": 1.379861, + "weight": 4.4619035391090396e-05 + }, + { + "days": 1.382639, + "weight": 7.266528620834722e-05 + }, + { + "days": 1.385417, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.386111, + "weight": 3.059590998246198e-05 + }, + { + "days": 1.386806, + "weight": 3.5695228312872314e-05 + }, + { + "days": 1.388194, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.388889, + "weight": 3.314556914766715e-05 + }, + { + "days": 1.389583, + "weight": 6.501630871273172e-05 + }, + { + "days": 1.390972, + "weight": 5.226801288670589e-05 + }, + { + "days": 1.391667, + "weight": 8.031426370396271e-05 + }, + { + "days": 1.392361, + "weight": 9.816187786039887e-05 + }, + { + "days": 1.393056, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.395139, + "weight": 3.187073956506457e-05 + }, + { + "days": 1.396528, + "weight": 2.4221762069449073e-05 + }, + { + "days": 1.397222, + "weight": 0.00022437000653805457 + }, + { + "days": 1.397917, + "weight": 3.824488747807748e-05 + }, + { + "days": 1.398611, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.399306, + "weight": 0.00032253188439845343 + }, + { + "days": 1.4, + "weight": 6.246664954752656e-05 + }, + { + "days": 1.402083, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.402778, + "weight": 2.1672102904243905e-05 + }, + { + "days": 1.403472, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.404167, + "weight": 0.0002358434727814778 + }, + { + "days": 1.405556, + "weight": 3.059590998246198e-05 + }, + { + "days": 1.407639, + "weight": 8.541358203437304e-05 + }, + { + "days": 1.409028, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.409722, + "weight": 2.4221762069449073e-05 + }, + { + "days": 1.410417, + "weight": 3.824488747807748e-05 + }, + { + "days": 1.4125, + "weight": 0.00011600949201683502 + }, + { + "days": 1.413194, + "weight": 8.796324119957821e-05 + }, + { + "days": 1.414583, + "weight": 0.00017337682323395124 + }, + { + "days": 1.415972, + "weight": 2.5496591652051656e-05 + }, + { + "days": 1.418056, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.41875, + "weight": 4.206937622588523e-05 + }, + { + "days": 1.419444, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.420139, + "weight": 0.0002677142123465424 + }, + { + "days": 1.426389, + "weight": 8.413875245177046e-05 + }, + { + "days": 1.430556, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.435417, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.4375, + "weight": 3.059590998246198e-05 + }, + { + "days": 1.440278, + "weight": 3.314556914766715e-05 + }, + { + "days": 1.441667, + "weight": 3.187073956506457e-05 + }, + { + "days": 1.445833, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.447222, + "weight": 3.314556914766715e-05 + }, + { + "days": 1.448611, + "weight": 0.00022182034737284939 + }, + { + "days": 1.449306, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.45, + "weight": 3.9519717060680065e-05 + }, + { + "days": 1.451389, + "weight": 0.00011091017368642469 + }, + { + "days": 1.453472, + "weight": 5.609250163451364e-05 + }, + { + "days": 1.454167, + "weight": 0.0001542543794949125 + }, + { + "days": 1.45625, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.456944, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.458333, + "weight": 0.00010198636660820662 + }, + { + "days": 1.460417, + "weight": 0.0002294693248684649 + }, + { + "days": 1.461111, + "weight": 0.00020524756279901581 + }, + { + "days": 1.463889, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.464583, + "weight": 6.119181996492397e-05 + }, + { + "days": 1.465972, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.468056, + "weight": 4.844352413889815e-05 + }, + { + "days": 1.469444, + "weight": 4.844352413889815e-05 + }, + { + "days": 1.470139, + "weight": 0.00016572784573833576 + }, + { + "days": 1.471528, + "weight": 0.00010581085535601436 + }, + { + "days": 1.472222, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.472917, + "weight": 3.5695228312872314e-05 + }, + { + "days": 1.474306, + "weight": 4.206937622588523e-05 + }, + { + "days": 1.475694, + "weight": 5.864216079971881e-05 + }, + { + "days": 1.476389, + "weight": 3.059590998246198e-05 + }, + { + "days": 1.477778, + "weight": 7.266528620834722e-05 + }, + { + "days": 1.479167, + "weight": 3.187073956506457e-05 + }, + { + "days": 1.48125, + "weight": 0.0003008597814942095 + }, + { + "days": 1.482639, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.495833, + "weight": 5.226801288670589e-05 + }, + { + "days": 1.498611, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.499306, + "weight": 0.0001491550611645022 + }, + { + "days": 1.504167, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.507639, + "weight": 6.62911382953343e-05 + }, + { + "days": 1.508333, + "weight": 6.501630871273172e-05 + }, + { + "days": 1.509028, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.509722, + "weight": 7.521494537355238e-05 + }, + { + "days": 1.510417, + "weight": 2.4221762069449073e-05 + }, + { + "days": 1.513194, + "weight": 8.413875245177046e-05 + }, + { + "days": 1.513889, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.515278, + "weight": 2.5496591652051656e-05 + }, + { + "days": 1.518056, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.51875, + "weight": 3.187073956506457e-05 + }, + { + "days": 1.519444, + "weight": 8.541358203437304e-05 + }, + { + "days": 1.520139, + "weight": 0.00034547881688529994 + }, + { + "days": 1.521528, + "weight": 7.903943412136013e-05 + }, + { + "days": 1.522222, + "weight": 0.00012238363992984793 + }, + { + "days": 1.523611, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.524306, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.527083, + "weight": 9.56122186951937e-05 + }, + { + "days": 1.527778, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.528472, + "weight": 5.481767205191106e-05 + }, + { + "days": 1.529167, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.530556, + "weight": 3.314556914766715e-05 + }, + { + "days": 1.53125, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.532639, + "weight": 9.433738911259112e-05 + }, + { + "days": 1.534028, + "weight": 5.226801288670589e-05 + }, + { + "days": 1.534722, + "weight": 6.119181996492397e-05 + }, + { + "days": 1.535417, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.536111, + "weight": 5.481767205191106e-05 + }, + { + "days": 1.536806, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.538194, + "weight": 0.00032125705481585084 + }, + { + "days": 1.545833, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.550694, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.55625, + "weight": 3.187073956506457e-05 + }, + { + "days": 1.558333, + "weight": 0.0003021346110768121 + }, + { + "days": 1.564583, + "weight": 2.677142123465424e-05 + }, + { + "days": 1.565972, + "weight": 3.5695228312872314e-05 + }, + { + "days": 1.567361, + "weight": 3.9519717060680065e-05 + }, + { + "days": 1.568056, + "weight": 3.187073956506457e-05 + }, + { + "days": 1.570139, + "weight": 3.187073956506457e-05 + }, + { + "days": 1.572917, + "weight": 0.00010453602577341178 + }, + { + "days": 1.574306, + "weight": 5.609250163451364e-05 + }, + { + "days": 1.577778, + "weight": 3.314556914766715e-05 + }, + { + "days": 1.578472, + "weight": 3.69700578954749e-05 + }, + { + "days": 1.579167, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.580556, + "weight": 0.00010836051452121953 + }, + { + "days": 1.581944, + "weight": 0.000354402623963518 + }, + { + "days": 1.582639, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.583333, + "weight": 5.609250163451364e-05 + }, + { + "days": 1.585417, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.586806, + "weight": 2.5496591652051656e-05 + }, + { + "days": 1.5875, + "weight": 4.4619035391090396e-05 + }, + { + "days": 1.588194, + "weight": 0.00011218500326902729 + }, + { + "days": 1.590972, + "weight": 7.39401157909498e-05 + }, + { + "days": 1.592361, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.593056, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.594444, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.595833, + "weight": 0.0002893863152507863 + }, + { + "days": 1.596528, + "weight": 5.481767205191106e-05 + }, + { + "days": 1.597917, + "weight": 5.609250163451364e-05 + }, + { + "days": 1.599306, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.6, + "weight": 2.4221762069449073e-05 + }, + { + "days": 1.601389, + "weight": 2.1672102904243905e-05 + }, + { + "days": 1.615278, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.621528, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.625, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.627083, + "weight": 2.677142123465424e-05 + }, + { + "days": 1.634722, + "weight": 7.39401157909498e-05 + }, + { + "days": 1.6375, + "weight": 2.5496591652051656e-05 + }, + { + "days": 1.639583, + "weight": 5.226801288670589e-05 + }, + { + "days": 1.640972, + "weight": 5.481767205191106e-05 + }, + { + "days": 1.641667, + "weight": 2.4221762069449073e-05 + }, + { + "days": 1.642361, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.64375, + "weight": 0.00011983398076464277 + }, + { + "days": 1.644444, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.645833, + "weight": 4.844352413889815e-05 + }, + { + "days": 1.646528, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.647917, + "weight": 0.00015680403866011767 + }, + { + "days": 1.650694, + "weight": 4.4619035391090396e-05 + }, + { + "days": 1.653472, + "weight": 7.139045662574463e-05 + }, + { + "days": 1.654861, + "weight": 0.00015680403866011767 + }, + { + "days": 1.655556, + "weight": 0.00011728432159943762 + }, + { + "days": 1.65625, + "weight": 4.206937622588523e-05 + }, + { + "days": 1.656944, + "weight": 8.541358203437304e-05 + }, + { + "days": 1.658333, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.659722, + "weight": 0.00033655500980708184 + }, + { + "days": 1.6625, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.668056, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.670833, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.671528, + "weight": 9.816187786039887e-05 + }, + { + "days": 1.676389, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.684028, + "weight": 2.4221762069449073e-05 + }, + { + "days": 1.689583, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.691667, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.69375, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.696528, + "weight": 3.059590998246198e-05 + }, + { + "days": 1.697917, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.698611, + "weight": 2.677142123465424e-05 + }, + { + "days": 1.699306, + "weight": 6.62911382953343e-05 + }, + { + "days": 1.700694, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.703472, + "weight": 7.521494537355238e-05 + }, + { + "days": 1.704861, + "weight": 3.059590998246198e-05 + }, + { + "days": 1.705556, + "weight": 4.4619035391090396e-05 + }, + { + "days": 1.709722, + "weight": 0.0001389564245036815 + }, + { + "days": 1.711806, + "weight": 8.541358203437304e-05 + }, + { + "days": 1.7125, + "weight": 3.824488747807748e-05 + }, + { + "days": 1.713889, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.714583, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.715278, + "weight": 2.4221762069449073e-05 + }, + { + "days": 1.715972, + "weight": 7.266528620834722e-05 + }, + { + "days": 1.716667, + "weight": 0.0001338571061732712 + }, + { + "days": 1.71875, + "weight": 3.059590998246198e-05 + }, + { + "days": 1.720139, + "weight": 0.0005545508684321235 + }, + { + "days": 1.721528, + "weight": 4.5893864973692975e-05 + }, + { + "days": 1.731944, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.735417, + "weight": 8.158909328656529e-05 + }, + { + "days": 1.738889, + "weight": 5.226801288670589e-05 + }, + { + "days": 1.743056, + "weight": 2.677142123465424e-05 + }, + { + "days": 1.744444, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.748611, + "weight": 3.5695228312872314e-05 + }, + { + "days": 1.757639, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.758333, + "weight": 5.099318330410331e-05 + }, + { + "days": 1.760417, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.761111, + "weight": 6.119181996492397e-05 + }, + { + "days": 1.7625, + "weight": 0.00015170472032970735 + }, + { + "days": 1.763194, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.766667, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.772222, + "weight": 6.119181996492397e-05 + }, + { + "days": 1.773611, + "weight": 0.00010198636660820662 + }, + { + "days": 1.775, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.775694, + "weight": 7.139045662574463e-05 + }, + { + "days": 1.777083, + "weight": 6.756596787793689e-05 + }, + { + "days": 1.777778, + "weight": 7.011562704314205e-05 + }, + { + "days": 1.779167, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.781944, + "weight": 0.0001402312540862841 + }, + { + "days": 1.782639, + "weight": 0.0004755114343107634 + }, + { + "days": 1.789583, + "weight": 8.923807078218079e-05 + }, + { + "days": 1.810417, + "weight": 4.206937622588523e-05 + }, + { + "days": 1.813194, + "weight": 2.677142123465424e-05 + }, + { + "days": 1.815278, + "weight": 2.804625081725682e-05 + }, + { + "days": 1.819444, + "weight": 7.776460453875755e-05 + }, + { + "days": 1.820833, + "weight": 2.677142123465424e-05 + }, + { + "days": 1.821528, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.823611, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.825, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.825694, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.827083, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.828472, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.829167, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.83125, + "weight": 4.0794546643282644e-05 + }, + { + "days": 1.831944, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.834028, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.834722, + "weight": 7.903943412136013e-05 + }, + { + "days": 1.835417, + "weight": 3.059590998246198e-05 + }, + { + "days": 1.836806, + "weight": 0.00014278091325148925 + }, + { + "days": 1.8375, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.838889, + "weight": 6.119181996492397e-05 + }, + { + "days": 1.839583, + "weight": 3.69700578954749e-05 + }, + { + "days": 1.840278, + "weight": 6.501630871273172e-05 + }, + { + "days": 1.840972, + "weight": 0.00011091017368642469 + }, + { + "days": 1.844444, + "weight": 0.0004933590484671995 + }, + { + "days": 1.845833, + "weight": 3.314556914766715e-05 + }, + { + "days": 1.847917, + "weight": 5.864216079971881e-05 + }, + { + "days": 1.849306, + "weight": 4.0794546643282644e-05 + }, + { + "days": 1.85625, + "weight": 6.884079746053947e-05 + }, + { + "days": 1.857639, + "weight": 2.677142123465424e-05 + }, + { + "days": 1.863889, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.867361, + "weight": 2.677142123465424e-05 + }, + { + "days": 1.868056, + "weight": 4.0794546643282644e-05 + }, + { + "days": 1.872222, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.872917, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.878472, + "weight": 8.158909328656529e-05 + }, + { + "days": 1.880556, + "weight": 3.4420398730269734e-05 + }, + { + "days": 1.88125, + "weight": 3.824488747807748e-05 + }, + { + "days": 1.882639, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.883333, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.884722, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.885417, + "weight": 2.5496591652051656e-05 + }, + { + "days": 1.886806, + "weight": 2.5496591652051656e-05 + }, + { + "days": 1.888194, + "weight": 4.716869455629556e-05 + }, + { + "days": 1.890278, + "weight": 0.00019632375572079775 + }, + { + "days": 1.891667, + "weight": 7.903943412136013e-05 + }, + { + "days": 1.894444, + "weight": 4.0794546643282644e-05 + }, + { + "days": 1.895833, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.897222, + "weight": 5.481767205191106e-05 + }, + { + "days": 1.897917, + "weight": 0.00019632375572079775 + }, + { + "days": 1.898611, + "weight": 6.374147913012914e-05 + }, + { + "days": 1.899306, + "weight": 0.00013513193575587378 + }, + { + "days": 1.9, + "weight": 3.5695228312872314e-05 + }, + { + "days": 1.900694, + "weight": 9.68870482777963e-05 + }, + { + "days": 1.906944, + "weight": 7.776460453875755e-05 + }, + { + "days": 1.907639, + "weight": 0.0007648977495615496 + }, + { + "days": 1.908333, + "weight": 3.9519717060680065e-05 + }, + { + "days": 1.909722, + "weight": 4.0794546643282644e-05 + }, + { + "days": 1.916667, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.921528, + "weight": 2.1672102904243905e-05 + }, + { + "days": 1.922917, + "weight": 5.481767205191106e-05 + }, + { + "days": 1.923611, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.925694, + "weight": 2.4221762069449073e-05 + }, + { + "days": 1.932639, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.933333, + "weight": 9.433738911259112e-05 + }, + { + "days": 1.936111, + "weight": 3.187073956506457e-05 + }, + { + "days": 1.939583, + "weight": 4.0794546643282644e-05 + }, + { + "days": 1.940278, + "weight": 8.923807078218079e-05 + }, + { + "days": 1.941667, + "weight": 2.677142123465424e-05 + }, + { + "days": 1.942361, + "weight": 7.521494537355238e-05 + }, + { + "days": 1.943056, + "weight": 4.206937622588523e-05 + }, + { + "days": 1.945139, + "weight": 4.206937622588523e-05 + }, + { + "days": 1.945833, + "weight": 7.903943412136013e-05 + }, + { + "days": 1.946528, + "weight": 0.00012493329909505312 + }, + { + "days": 1.947222, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.947917, + "weight": 3.824488747807748e-05 + }, + { + "days": 1.949306, + "weight": 5.354284246930848e-05 + }, + { + "days": 1.95, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.950694, + "weight": 7.139045662574463e-05 + }, + { + "days": 1.951389, + "weight": 7.776460453875755e-05 + }, + { + "days": 1.954861, + "weight": 0.00016445301615573317 + }, + { + "days": 1.955556, + "weight": 2.9321080399859404e-05 + }, + { + "days": 1.956944, + "weight": 0.00011218500326902729 + }, + { + "days": 1.958333, + "weight": 4.844352413889815e-05 + }, + { + "days": 1.959028, + "weight": 0.000323806713981056 + }, + { + "days": 1.959722, + "weight": 0.00013130744700806603 + }, + { + "days": 1.961111, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.963194, + "weight": 2.2946932486846487e-05 + }, + { + "days": 1.963889, + "weight": 3.5695228312872314e-05 + }, + { + "days": 1.964583, + "weight": 0.000161903356990528 + }, + { + "days": 1.965972, + "weight": 0.00031998222523324824 + }, + { + "days": 1.966667, + "weight": 3.824488747807748e-05 + }, + { + "days": 1.968056, + "weight": 3.69700578954749e-05 + }, + { + "days": 1.970139, + "weight": 4.334420580848781e-05 + }, + { + "days": 1.970833, + "weight": 3.69700578954749e-05 + }, + { + "days": 1.972917, + "weight": 7.39401157909498e-05 + }, + { + "days": 1.975694, + "weight": 3.059590998246198e-05 + }, + { + "days": 1.98125, + "weight": 4.4619035391090396e-05 + }, + { + "days": 1.981944, + "weight": 5.736733121711622e-05 + }, + { + "days": 1.982639, + "weight": 0.0001415060836688867 + }, + { + "days": 1.9875, + "weight": 7.39401157909498e-05 + }, + { + "days": 1.988194, + "weight": 7.776460453875755e-05 + }, + { + "days": 1.990972, + "weight": 4.716869455629556e-05 + }, + { + "days": 1.99375, + "weight": 8.413875245177046e-05 + }, + { + "days": 1.997222, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.997917, + "weight": 4.9718353721500726e-05 + }, + { + "days": 1.998611, + "weight": 0.00019632375572079775 + }, + { + "days": 1.999306, + "weight": 6.62911382953343e-05 + }, + { + "days": 2.001389, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.002083, + "weight": 9.051290036478337e-05 + }, + { + "days": 2.004167, + "weight": 0.0002983101223290044 + }, + { + "days": 2.004861, + "weight": 0.00015297954991230992 + }, + { + "days": 2.005556, + "weight": 2.677142123465424e-05 + }, + { + "days": 2.006944, + "weight": 2.804625081725682e-05 + }, + { + "days": 2.008333, + "weight": 6.374147913012914e-05 + }, + { + "days": 2.009028, + "weight": 0.00015042989074710476 + }, + { + "days": 2.009722, + "weight": 0.00013003261742546344 + }, + { + "days": 2.010417, + "weight": 0.00010708568493861695 + }, + { + "days": 2.013194, + "weight": 3.187073956506457e-05 + }, + { + "days": 2.014583, + "weight": 5.354284246930848e-05 + }, + { + "days": 2.015278, + "weight": 4.206937622588523e-05 + }, + { + "days": 2.015972, + "weight": 0.00020397273321641325 + }, + { + "days": 2.018056, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.01875, + "weight": 7.648977495615496e-05 + }, + { + "days": 2.020139, + "weight": 9.433738911259112e-05 + }, + { + "days": 2.020833, + "weight": 0.0002664393827639398 + }, + { + "days": 2.023611, + "weight": 9.433738911259112e-05 + }, + { + "days": 2.024306, + "weight": 4.716869455629556e-05 + }, + { + "days": 2.025694, + "weight": 0.0003990216593546084 + }, + { + "days": 2.03125, + "weight": 0.00023839313194668296 + }, + { + "days": 2.031944, + "weight": 4.4619035391090396e-05 + }, + { + "days": 2.034028, + "weight": 7.266528620834722e-05 + }, + { + "days": 2.041667, + "weight": 4.9718353721500726e-05 + }, + { + "days": 2.042361, + "weight": 7.266528620834722e-05 + }, + { + "days": 2.044444, + "weight": 3.824488747807748e-05 + }, + { + "days": 2.045139, + "weight": 3.9519717060680065e-05 + }, + { + "days": 2.046528, + "weight": 3.5695228312872314e-05 + }, + { + "days": 2.048611, + "weight": 3.69700578954749e-05 + }, + { + "days": 2.049306, + "weight": 5.736733121711622e-05 + }, + { + "days": 2.052778, + "weight": 6.756596787793689e-05 + }, + { + "days": 2.053472, + "weight": 3.824488747807748e-05 + }, + { + "days": 2.054167, + "weight": 2.804625081725682e-05 + }, + { + "days": 2.056944, + "weight": 7.776460453875755e-05 + }, + { + "days": 2.057639, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.059028, + "weight": 2.804625081725682e-05 + }, + { + "days": 2.059722, + "weight": 5.736733121711622e-05 + }, + { + "days": 2.063889, + "weight": 9.051290036478337e-05 + }, + { + "days": 2.065278, + "weight": 4.716869455629556e-05 + }, + { + "days": 2.065972, + "weight": 2.804625081725682e-05 + }, + { + "days": 2.066667, + "weight": 5.481767205191106e-05 + }, + { + "days": 2.068056, + "weight": 8.413875245177046e-05 + }, + { + "days": 2.070833, + "weight": 0.00012875778784286084 + }, + { + "days": 2.071528, + "weight": 4.9718353721500726e-05 + }, + { + "days": 2.072917, + "weight": 6.119181996492397e-05 + }, + { + "days": 2.073611, + "weight": 3.824488747807748e-05 + }, + { + "days": 2.075, + "weight": 0.00012875778784286084 + }, + { + "days": 2.077083, + "weight": 9.178772994738595e-05 + }, + { + "days": 2.078472, + "weight": 7.776460453875755e-05 + }, + { + "days": 2.079861, + "weight": 6.62911382953343e-05 + }, + { + "days": 2.080556, + "weight": 3.4420398730269734e-05 + }, + { + "days": 2.082639, + "weight": 5.481767205191106e-05 + }, + { + "days": 2.083333, + "weight": 2.1672102904243905e-05 + }, + { + "days": 2.086806, + "weight": 0.0002804625081725682 + }, + { + "days": 2.093056, + "weight": 0.0003951971706068006 + }, + { + "days": 2.095139, + "weight": 2.677142123465424e-05 + }, + { + "days": 2.108333, + "weight": 6.119181996492397e-05 + }, + { + "days": 2.110417, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.115972, + "weight": 3.5695228312872314e-05 + }, + { + "days": 2.117361, + "weight": 2.1672102904243905e-05 + }, + { + "days": 2.11875, + "weight": 0.00013513193575587378 + }, + { + "days": 2.120833, + "weight": 4.4619035391090396e-05 + }, + { + "days": 2.122917, + "weight": 2.2946932486846487e-05 + }, + { + "days": 2.125, + "weight": 3.4420398730269734e-05 + }, + { + "days": 2.126389, + "weight": 2.677142123465424e-05 + }, + { + "days": 2.127778, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.129861, + "weight": 3.5695228312872314e-05 + }, + { + "days": 2.130556, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.132639, + "weight": 2.2946932486846487e-05 + }, + { + "days": 2.134028, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.136111, + "weight": 8.158909328656529e-05 + }, + { + "days": 2.1375, + "weight": 0.00011728432159943762 + }, + { + "days": 2.138194, + "weight": 6.62911382953343e-05 + }, + { + "days": 2.138889, + "weight": 3.314556914766715e-05 + }, + { + "days": 2.140278, + "weight": 0.00012110881034724535 + }, + { + "days": 2.140972, + "weight": 3.69700578954749e-05 + }, + { + "days": 2.145139, + "weight": 3.314556914766715e-05 + }, + { + "days": 2.15, + "weight": 9.56122186951937e-05 + }, + { + "days": 2.152778, + "weight": 2.2946932486846487e-05 + }, + { + "days": 2.154861, + "weight": 0.00014533057241669444 + }, + { + "days": 2.159028, + "weight": 7.903943412136013e-05 + }, + { + "days": 2.164583, + "weight": 2.804625081725682e-05 + }, + { + "days": 2.165972, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.168056, + "weight": 2.4221762069449073e-05 + }, + { + "days": 2.178472, + "weight": 2.1672102904243905e-05 + }, + { + "days": 2.180556, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.181944, + "weight": 8.413875245177046e-05 + }, + { + "days": 2.182639, + "weight": 4.206937622588523e-05 + }, + { + "days": 2.184722, + "weight": 8.541358203437304e-05 + }, + { + "days": 2.188194, + "weight": 2.677142123465424e-05 + }, + { + "days": 2.189583, + "weight": 4.716869455629556e-05 + }, + { + "days": 2.190972, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.191667, + "weight": 2.804625081725682e-05 + }, + { + "days": 2.195139, + "weight": 4.844352413889815e-05 + }, + { + "days": 2.196528, + "weight": 2.2946932486846487e-05 + }, + { + "days": 2.197222, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.197917, + "weight": 2.804625081725682e-05 + }, + { + "days": 2.198611, + "weight": 5.226801288670589e-05 + }, + { + "days": 2.199306, + "weight": 4.334420580848781e-05 + }, + { + "days": 2.2, + "weight": 4.9718353721500726e-05 + }, + { + "days": 2.204167, + "weight": 3.059590998246198e-05 + }, + { + "days": 2.206944, + "weight": 0.00010836051452121953 + }, + { + "days": 2.210417, + "weight": 0.00020269790363381066 + }, + { + "days": 2.211806, + "weight": 2.1672102904243905e-05 + }, + { + "days": 2.220833, + "weight": 0.0001032611961908092 + }, + { + "days": 2.238889, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.241667, + "weight": 3.69700578954749e-05 + }, + { + "days": 2.24375, + "weight": 5.226801288670589e-05 + }, + { + "days": 2.245139, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.246528, + "weight": 3.9519717060680065e-05 + }, + { + "days": 2.247917, + "weight": 7.139045662574463e-05 + }, + { + "days": 2.250694, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.252778, + "weight": 4.0794546643282644e-05 + }, + { + "days": 2.253472, + "weight": 3.059590998246198e-05 + }, + { + "days": 2.254167, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.25625, + "weight": 6.119181996492397e-05 + }, + { + "days": 2.256944, + "weight": 9.943670744300145e-05 + }, + { + "days": 2.258333, + "weight": 3.059590998246198e-05 + }, + { + "days": 2.259028, + "weight": 3.9519717060680065e-05 + }, + { + "days": 2.261806, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.266667, + "weight": 7.521494537355238e-05 + }, + { + "days": 2.269444, + "weight": 0.00016827750490354092 + }, + { + "days": 2.272222, + "weight": 0.0001402312540862841 + }, + { + "days": 2.299306, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.302778, + "weight": 4.334420580848781e-05 + }, + { + "days": 2.309722, + "weight": 3.314556914766715e-05 + }, + { + "days": 2.311806, + "weight": 5.354284246930848e-05 + }, + { + "days": 2.3125, + "weight": 4.4619035391090396e-05 + }, + { + "days": 2.313194, + "weight": 2.4221762069449073e-05 + }, + { + "days": 2.314583, + "weight": 4.716869455629556e-05 + }, + { + "days": 2.315972, + "weight": 4.716869455629556e-05 + }, + { + "days": 2.318056, + "weight": 9.56122186951937e-05 + }, + { + "days": 2.31875, + "weight": 4.0794546643282644e-05 + }, + { + "days": 2.325694, + "weight": 7.266528620834722e-05 + }, + { + "days": 2.330556, + "weight": 0.00018994960780778484 + }, + { + "days": 2.33125, + "weight": 9.433738911259112e-05 + }, + { + "days": 2.336111, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.338194, + "weight": 9.178772994738595e-05 + }, + { + "days": 2.360417, + "weight": 4.0794546643282644e-05 + }, + { + "days": 2.36875, + "weight": 3.69700578954749e-05 + }, + { + "days": 2.373611, + "weight": 7.903943412136013e-05 + }, + { + "days": 2.375, + "weight": 2.4221762069449073e-05 + }, + { + "days": 2.376389, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.379167, + "weight": 5.864216079971881e-05 + }, + { + "days": 2.379861, + "weight": 2.4221762069449073e-05 + }, + { + "days": 2.384722, + "weight": 0.00012875778784286084 + }, + { + "days": 2.385417, + "weight": 3.69700578954749e-05 + }, + { + "days": 2.386111, + "weight": 0.00020142307405120806 + }, + { + "days": 2.392361, + "weight": 0.00010836051452121953 + }, + { + "days": 2.39375, + "weight": 2.2946932486846487e-05 + }, + { + "days": 2.397222, + "weight": 5.991699038232139e-05 + }, + { + "days": 2.421528, + "weight": 3.5695228312872314e-05 + }, + { + "days": 2.43125, + "weight": 5.991699038232139e-05 + }, + { + "days": 2.432639, + "weight": 4.716869455629556e-05 + }, + { + "days": 2.434722, + "weight": 6.119181996492397e-05 + }, + { + "days": 2.435417, + "weight": 2.1672102904243905e-05 + }, + { + "days": 2.436806, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.4375, + "weight": 3.059590998246198e-05 + }, + { + "days": 2.440278, + "weight": 0.0001338571061732712 + }, + { + "days": 2.44375, + "weight": 3.059590998246198e-05 + }, + { + "days": 2.447917, + "weight": 9.306255952998854e-05 + }, + { + "days": 2.448611, + "weight": 3.059590998246198e-05 + }, + { + "days": 2.450694, + "weight": 0.00020779722196422097 + }, + { + "days": 2.451389, + "weight": 4.334420580848781e-05 + }, + { + "days": 2.458333, + "weight": 0.00010581085535601436 + }, + { + "days": 2.478472, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.49375, + "weight": 3.059590998246198e-05 + }, + { + "days": 2.495139, + "weight": 2.2946932486846487e-05 + }, + { + "days": 2.495833, + "weight": 5.609250163451364e-05 + }, + { + "days": 2.496528, + "weight": 4.844352413889815e-05 + }, + { + "days": 2.499306, + "weight": 0.00020779722196422097 + }, + { + "days": 2.501389, + "weight": 3.187073956506457e-05 + }, + { + "days": 2.505556, + "weight": 0.0002791876785899656 + }, + { + "days": 2.509722, + "weight": 4.9718353721500726e-05 + }, + { + "days": 2.511111, + "weight": 3.824488747807748e-05 + }, + { + "days": 2.5125, + "weight": 0.00010708568493861695 + }, + { + "days": 2.513194, + "weight": 3.4420398730269734e-05 + }, + { + "days": 2.513889, + "weight": 8.158909328656529e-05 + }, + { + "days": 2.532639, + "weight": 2.804625081725682e-05 + }, + { + "days": 2.538194, + "weight": 3.059590998246198e-05 + }, + { + "days": 2.552083, + "weight": 2.2946932486846487e-05 + }, + { + "days": 2.554167, + "weight": 3.9519717060680065e-05 + }, + { + "days": 2.557639, + "weight": 3.059590998246198e-05 + }, + { + "days": 2.561111, + "weight": 2.4221762069449073e-05 + }, + { + "days": 2.5625, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.565278, + "weight": 5.481767205191106e-05 + }, + { + "days": 2.565972, + "weight": 0.00035695228312872316 + }, + { + "days": 2.567361, + "weight": 0.00016062852740792542 + }, + { + "days": 2.568056, + "weight": 4.206937622588523e-05 + }, + { + "days": 2.570139, + "weight": 6.246664954752656e-05 + }, + { + "days": 2.570833, + "weight": 0.00015170472032970735 + }, + { + "days": 2.578472, + "weight": 0.00011345983285162986 + }, + { + "days": 2.602083, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.620139, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.621528, + "weight": 0.0001389564245036815 + }, + { + "days": 2.628472, + "weight": 0.0001415060836688867 + }, + { + "days": 2.629167, + "weight": 7.139045662574463e-05 + }, + { + "days": 2.629861, + "weight": 9.306255952998854e-05 + }, + { + "days": 2.63125, + "weight": 4.844352413889815e-05 + }, + { + "days": 2.632639, + "weight": 0.0001402312540862841 + }, + { + "days": 2.633333, + "weight": 0.00012365846951245053 + }, + { + "days": 2.636806, + "weight": 0.00043726654683268586 + }, + { + "days": 2.672917, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.674306, + "weight": 4.5893864973692975e-05 + }, + { + "days": 2.680556, + "weight": 0.0002141713698772339 + }, + { + "days": 2.684722, + "weight": 8.668841161697562e-05 + }, + { + "days": 2.686806, + "weight": 6.374147913012914e-05 + }, + { + "days": 2.688194, + "weight": 0.0003786243860329671 + }, + { + "days": 2.689583, + "weight": 4.4619035391090396e-05 + }, + { + "days": 2.690278, + "weight": 0.0001032611961908092 + }, + { + "days": 2.69375, + "weight": 2.804625081725682e-05 + }, + { + "days": 2.694444, + "weight": 7.139045662574463e-05 + }, + { + "days": 2.698611, + "weight": 0.00010071153702560403 + }, + { + "days": 2.700694, + "weight": 3.187073956506457e-05 + }, + { + "days": 2.713889, + "weight": 2.1672102904243905e-05 + }, + { + "days": 2.729861, + "weight": 4.334420580848781e-05 + }, + { + "days": 2.731944, + "weight": 2.1672102904243905e-05 + }, + { + "days": 2.739583, + "weight": 0.00015807886824272026 + }, + { + "days": 2.747222, + "weight": 0.0005532760388495209 + }, + { + "days": 2.748611, + "weight": 5.609250163451364e-05 + }, + { + "days": 2.75, + "weight": 6.374147913012914e-05 + }, + { + "days": 2.750694, + "weight": 4.0794546643282644e-05 + }, + { + "days": 2.751389, + "weight": 4.206937622588523e-05 + }, + { + "days": 2.754861, + "weight": 8.541358203437304e-05 + }, + { + "days": 2.75625, + "weight": 0.0001389564245036815 + }, + { + "days": 2.757639, + "weight": 2.2946932486846487e-05 + }, + { + "days": 2.780556, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.793056, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.794444, + "weight": 3.9519717060680065e-05 + }, + { + "days": 2.804861, + "weight": 5.226801288670589e-05 + }, + { + "days": 2.805556, + "weight": 2.4221762069449073e-05 + }, + { + "days": 2.80625, + "weight": 0.001004565711090835 + }, + { + "days": 2.807639, + "weight": 6.756596787793689e-05 + }, + { + "days": 2.809028, + "weight": 2.2946932486846487e-05 + }, + { + "days": 2.813889, + "weight": 0.0002893863152507863 + }, + { + "days": 2.816667, + "weight": 6.756596787793689e-05 + }, + { + "days": 2.822222, + "weight": 3.5695228312872314e-05 + }, + { + "days": 2.848611, + "weight": 4.4619035391090396e-05 + }, + { + "days": 2.849306, + "weight": 4.4619035391090396e-05 + }, + { + "days": 2.859028, + "weight": 2.4221762069449073e-05 + }, + { + "days": 2.864583, + "weight": 3.5695228312872314e-05 + }, + { + "days": 2.867361, + "weight": 5.864216079971881e-05 + }, + { + "days": 2.868056, + "weight": 3.69700578954749e-05 + }, + { + "days": 2.870833, + "weight": 5.991699038232139e-05 + }, + { + "days": 2.871528, + "weight": 0.00013640676533847635 + }, + { + "days": 2.873611, + "weight": 3.5695228312872314e-05 + }, + { + "days": 2.875, + "weight": 2.804625081725682e-05 + }, + { + "days": 2.875694, + "weight": 0.00023074415445106748 + }, + { + "days": 2.880556, + "weight": 0.004813756503907352 + }, + { + "days": 2.882639, + "weight": 0.00011218500326902729 + }, + { + "days": 2.890972, + "weight": 5.226801288670589e-05 + }, + { + "days": 2.895833, + "weight": 4.4619035391090396e-05 + }, + { + "days": 2.913194, + "weight": 8.031426370396271e-05 + }, + { + "days": 2.914583, + "weight": 0.00012365846951245053 + }, + { + "days": 2.91875, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.919444, + "weight": 3.9519717060680065e-05 + }, + { + "days": 2.922917, + "weight": 0.0001325822765906686 + }, + { + "days": 2.925694, + "weight": 0.0002358434727814778 + }, + { + "days": 2.929167, + "weight": 0.00020269790363381066 + }, + { + "days": 2.934028, + "weight": 0.00018739994864257965 + }, + { + "days": 2.9375, + "weight": 5.099318330410331e-05 + }, + { + "days": 2.940972, + "weight": 0.00010581085535601436 + }, + { + "days": 2.941667, + "weight": 0.0006833086562749843 + }, + { + "days": 2.942361, + "weight": 0.003034094406594147 + }, + { + "days": 2.95, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.954167, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.957639, + "weight": 6.246664954752656e-05 + }, + { + "days": 2.959028, + "weight": 3.314556914766715e-05 + }, + { + "days": 2.959722, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.960417, + "weight": 2.9321080399859404e-05 + }, + { + "days": 2.965278, + "weight": 2.4221762069449073e-05 + }, + { + "days": 2.965972, + "weight": 3.314556914766715e-05 + }, + { + "days": 2.968056, + "weight": 0.0002715387010943501 + }, + { + "days": 2.969444, + "weight": 3.824488747807748e-05 + }, + { + "days": 2.971528, + "weight": 2.677142123465424e-05 + }, + { + "days": 2.972222, + "weight": 0.00032635637314626115 + }, + { + "days": 2.972917, + "weight": 3.314556914766715e-05 + }, + { + "days": 2.974306, + "weight": 8.541358203437304e-05 + }, + { + "days": 2.976389, + "weight": 2.5496591652051656e-05 + }, + { + "days": 2.977083, + "weight": 4.334420580848781e-05 + }, + { + "days": 2.978472, + "weight": 4.9718353721500726e-05 + }, + { + "days": 2.982639, + "weight": 4.9718353721500726e-05 + }, + { + "days": 2.984722, + "weight": 0.00018739994864257965 + }, + { + "days": 2.985417, + "weight": 3.059590998246198e-05 + }, + { + "days": 2.986806, + "weight": 3.5695228312872314e-05 + }, + { + "days": 2.9875, + "weight": 0.00034802847605050507 + }, + { + "days": 2.990972, + "weight": 0.0005787726305015726 + }, + { + "days": 2.993056, + "weight": 0.00043726654683268586 + }, + { + "days": 2.995139, + "weight": 0.0004053958072676213 + }, + { + "days": 2.997917, + "weight": 0.00017210199365134867 + }, + { + "days": 3.0, + "weight": 0.00032763120272886375 + }, + { + "days": 3.008333, + "weight": 0.0017222947660960892 + }, + { + "days": 3.011111, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.015278, + "weight": 6.246664954752656e-05 + }, + { + "days": 3.017361, + "weight": 4.334420580848781e-05 + }, + { + "days": 3.030556, + "weight": 4.9718353721500726e-05 + }, + { + "days": 3.031944, + "weight": 3.69700578954749e-05 + }, + { + "days": 3.032639, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.033333, + "weight": 0.00012875778784286084 + }, + { + "days": 3.036111, + "weight": 0.00010198636660820662 + }, + { + "days": 3.0375, + "weight": 4.0794546643282644e-05 + }, + { + "days": 3.038194, + "weight": 0.00022564483612065714 + }, + { + "days": 3.042361, + "weight": 0.0004665876272325453 + }, + { + "days": 3.04375, + "weight": 6.246664954752656e-05 + }, + { + "days": 3.048611, + "weight": 0.00034802847605050507 + }, + { + "days": 3.049306, + "weight": 9.943670744300145e-05 + }, + { + "days": 3.050694, + "weight": 0.0002434924502770933 + }, + { + "days": 3.052083, + "weight": 2.2946932486846487e-05 + }, + { + "days": 3.055556, + "weight": 4.9718353721500726e-05 + }, + { + "days": 3.056944, + "weight": 6.62911382953343e-05 + }, + { + "days": 3.059722, + "weight": 0.0002906611448333889 + }, + { + "days": 3.061111, + "weight": 0.00016445301615573317 + }, + { + "days": 3.090972, + "weight": 6.62911382953343e-05 + }, + { + "days": 3.095139, + "weight": 2.4221762069449073e-05 + }, + { + "days": 3.098611, + "weight": 2.677142123465424e-05 + }, + { + "days": 3.104167, + "weight": 3.69700578954749e-05 + }, + { + "days": 3.105556, + "weight": 3.059590998246198e-05 + }, + { + "days": 3.106944, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.107639, + "weight": 0.00013513193575587378 + }, + { + "days": 3.109028, + "weight": 4.206937622588523e-05 + }, + { + "days": 3.1125, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.114583, + "weight": 0.00010708568493861695 + }, + { + "days": 3.115278, + "weight": 5.099318330410331e-05 + }, + { + "days": 3.115972, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.116667, + "weight": 5.354284246930848e-05 + }, + { + "days": 3.123611, + "weight": 5.354284246930848e-05 + }, + { + "days": 3.138194, + "weight": 2.4221762069449073e-05 + }, + { + "days": 3.15, + "weight": 2.677142123465424e-05 + }, + { + "days": 3.156944, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.157639, + "weight": 3.314556914766715e-05 + }, + { + "days": 3.165278, + "weight": 5.864216079971881e-05 + }, + { + "days": 3.169444, + "weight": 3.9519717060680065e-05 + }, + { + "days": 3.170139, + "weight": 3.314556914766715e-05 + }, + { + "days": 3.174306, + "weight": 2.677142123465424e-05 + }, + { + "days": 3.175, + "weight": 5.736733121711622e-05 + }, + { + "days": 3.177778, + "weight": 2.2946932486846487e-05 + }, + { + "days": 3.18125, + "weight": 9.816187786039887e-05 + }, + { + "days": 3.213194, + "weight": 0.00013130744700806603 + }, + { + "days": 3.224306, + "weight": 7.776460453875755e-05 + }, + { + "days": 3.23125, + "weight": 4.334420580848781e-05 + }, + { + "days": 3.233333, + "weight": 2.677142123465424e-05 + }, + { + "days": 3.234028, + "weight": 3.824488747807748e-05 + }, + { + "days": 3.235417, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.236111, + "weight": 2.2946932486846487e-05 + }, + { + "days": 3.275, + "weight": 4.0794546643282644e-05 + }, + { + "days": 3.2875, + "weight": 7.776460453875755e-05 + }, + { + "days": 3.291667, + "weight": 2.4221762069449073e-05 + }, + { + "days": 3.293056, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.297917, + "weight": 2.2946932486846487e-05 + }, + { + "days": 3.300694, + "weight": 9.306255952998854e-05 + }, + { + "days": 3.343056, + "weight": 4.716869455629556e-05 + }, + { + "days": 3.347222, + "weight": 3.824488747807748e-05 + }, + { + "days": 3.349306, + "weight": 4.0794546643282644e-05 + }, + { + "days": 3.359028, + "weight": 2.677142123465424e-05 + }, + { + "days": 3.404167, + "weight": 2.9321080399859404e-05 + }, + { + "days": 3.406944, + "weight": 2.4221762069449073e-05 + }, + { + "days": 3.409028, + "weight": 2.804625081725682e-05 + }, + { + "days": 3.417361, + "weight": 7.011562704314205e-05 + }, + { + "days": 3.419444, + "weight": 3.69700578954749e-05 + }, + { + "days": 3.478472, + "weight": 3.5695228312872314e-05 + }, + { + "days": 3.479167, + "weight": 2.677142123465424e-05 + }, + { + "days": 3.485417, + "weight": 3.4420398730269734e-05 + }, + { + "days": 3.524306, + "weight": 2.4221762069449073e-05 + }, + { + "days": 3.533333, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.544444, + "weight": 3.824488747807748e-05 + }, + { + "days": 3.545139, + "weight": 6.756596787793689e-05 + }, + { + "days": 3.590278, + "weight": 3.187073956506457e-05 + }, + { + "days": 3.611111, + "weight": 7.011562704314205e-05 + }, + { + "days": 3.660417, + "weight": 3.314556914766715e-05 + }, + { + "days": 3.670139, + "weight": 0.00012748295826025828 + }, + { + "days": 3.719444, + "weight": 4.206937622588523e-05 + }, + { + "days": 3.723611, + "weight": 2.4221762069449073e-05 + }, + { + "days": 3.727778, + "weight": 0.00015042989074710476 + }, + { + "days": 3.757639, + "weight": 2.9321080399859404e-05 + }, + { + "days": 3.767361, + "weight": 3.314556914766715e-05 + }, + { + "days": 3.779167, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.782639, + "weight": 2.2946932486846487e-05 + }, + { + "days": 3.784722, + "weight": 3.314556914766715e-05 + }, + { + "days": 3.786111, + "weight": 0.00016955233448614351 + }, + { + "days": 3.798611, + "weight": 2.1672102904243905e-05 + }, + { + "days": 3.804861, + "weight": 2.4221762069449073e-05 + }, + { + "days": 3.809028, + "weight": 3.4420398730269734e-05 + }, + { + "days": 3.822222, + "weight": 3.69700578954749e-05 + }, + { + "days": 3.831944, + "weight": 3.187073956506457e-05 + }, + { + "days": 3.839583, + "weight": 2.4221762069449073e-05 + }, + { + "days": 3.840278, + "weight": 2.5496591652051656e-05 + }, + { + "days": 3.849306, + "weight": 0.00033018086189406893 + }, + { + "days": 3.857639, + "weight": 3.5695228312872314e-05 + }, + { + "days": 3.86875, + "weight": 2.804625081725682e-05 + }, + { + "days": 3.884028, + "weight": 3.4420398730269734e-05 + }, + { + "days": 3.89375, + "weight": 3.5695228312872314e-05 + }, + { + "days": 3.898611, + "weight": 0.0006463385983795095 + }, + { + "days": 3.899306, + "weight": 2.4221762069449073e-05 + }, + { + "days": 3.927083, + "weight": 6.374147913012914e-05 + }, + { + "days": 3.930556, + "weight": 4.844352413889815e-05 + }, + { + "days": 3.931944, + "weight": 0.00013640676533847635 + }, + { + "days": 3.932639, + "weight": 4.334420580848781e-05 + }, + { + "days": 3.943056, + "weight": 5.226801288670589e-05 + }, + { + "days": 3.94375, + "weight": 2.804625081725682e-05 + }, + { + "days": 3.945833, + "weight": 4.844352413889815e-05 + }, + { + "days": 3.952083, + "weight": 2.677142123465424e-05 + }, + { + "days": 3.952778, + "weight": 8.668841161697562e-05 + }, + { + "days": 3.953472, + "weight": 3.9519717060680065e-05 + }, + { + "days": 3.95625, + "weight": 3.059590998246198e-05 + }, + { + "days": 3.959722, + "weight": 9.433738911259112e-05 + }, + { + "days": 3.961111, + "weight": 3.059590998246198e-05 + }, + { + "days": 3.963889, + "weight": 6.246664954752656e-05 + }, + { + "days": 3.965278, + "weight": 0.000308508758989825 + }, + { + "days": 3.968056, + "weight": 2.804625081725682e-05 + }, + { + "days": 3.984028, + "weight": 2.804625081725682e-05 + }, + { + "days": 3.99375, + "weight": 0.00012238363992984793 + }, + { + "days": 3.995833, + "weight": 6.501630871273172e-05 + }, + { + "days": 3.996528, + "weight": 4.716869455629556e-05 + }, + { + "days": 3.998611, + "weight": 6.246664954752656e-05 + }, + { + "days": 4.001389, + "weight": 0.00015170472032970735 + }, + { + "days": 4.002778, + "weight": 2.804625081725682e-05 + }, + { + "days": 4.004167, + "weight": 3.5695228312872314e-05 + }, + { + "days": 4.00625, + "weight": 2.804625081725682e-05 + }, + { + "days": 4.007639, + "weight": 5.481767205191106e-05 + }, + { + "days": 4.008333, + "weight": 2.5496591652051656e-05 + }, + { + "days": 4.011806, + "weight": 0.00010198636660820662 + }, + { + "days": 4.015278, + "weight": 4.9718353721500726e-05 + }, + { + "days": 4.018056, + "weight": 4.334420580848781e-05 + }, + { + "days": 4.01875, + "weight": 4.9718353721500726e-05 + }, + { + "days": 4.019444, + "weight": 0.00017082716406874608 + }, + { + "days": 4.020833, + "weight": 8.796324119957821e-05 + }, + { + "days": 4.021528, + "weight": 3.5695228312872314e-05 + }, + { + "days": 4.025, + "weight": 0.00013640676533847635 + }, + { + "days": 4.029861, + "weight": 2.677142123465424e-05 + }, + { + "days": 4.035417, + "weight": 2.9321080399859404e-05 + }, + { + "days": 4.047917, + "weight": 4.206937622588523e-05 + }, + { + "days": 4.054167, + "weight": 2.804625081725682e-05 + }, + { + "days": 4.059722, + "weight": 4.0794546643282644e-05 + }, + { + "days": 4.063194, + "weight": 6.374147913012914e-05 + }, + { + "days": 4.069444, + "weight": 2.1672102904243905e-05 + }, + { + "days": 4.070139, + "weight": 5.226801288670589e-05 + }, + { + "days": 4.074306, + "weight": 6.374147913012914e-05 + }, + { + "days": 4.078472, + "weight": 2.4221762069449073e-05 + }, + { + "days": 4.079861, + "weight": 3.059590998246198e-05 + }, + { + "days": 4.080556, + "weight": 2.4221762069449073e-05 + }, + { + "days": 4.082639, + "weight": 9.56122186951937e-05 + }, + { + "days": 4.095833, + "weight": 2.2946932486846487e-05 + }, + { + "days": 4.11875, + "weight": 2.4221762069449073e-05 + }, + { + "days": 4.127083, + "weight": 4.5893864973692975e-05 + }, + { + "days": 4.129167, + "weight": 3.059590998246198e-05 + }, + { + "days": 4.131944, + "weight": 3.9519717060680065e-05 + }, + { + "days": 4.1375, + "weight": 3.187073956506457e-05 + }, + { + "days": 4.148611, + "weight": 6.62911382953343e-05 + }, + { + "days": 4.181944, + "weight": 3.69700578954749e-05 + }, + { + "days": 4.209028, + "weight": 2.4221762069449073e-05 + }, + { + "days": 4.211806, + "weight": 2.677142123465424e-05 + }, + { + "days": 4.23125, + "weight": 3.824488747807748e-05 + }, + { + "days": 4.268056, + "weight": 2.2946932486846487e-05 + }, + { + "days": 4.273611, + "weight": 2.5496591652051656e-05 + }, + { + "days": 4.305556, + "weight": 2.2946932486846487e-05 + }, + { + "days": 4.334722, + "weight": 7.521494537355238e-05 + }, + { + "days": 4.438889, + "weight": 2.804625081725682e-05 + }, + { + "days": 4.450694, + "weight": 2.1672102904243905e-05 + }, + { + "days": 4.467361, + "weight": 2.5496591652051656e-05 + }, + { + "days": 4.619444, + "weight": 3.824488747807748e-05 + }, + { + "days": 4.731944, + "weight": 2.4221762069449073e-05 + }, + { + "days": 4.760417, + "weight": 2.5496591652051656e-05 + }, + { + "days": 4.770833, + "weight": 0.00015680403866011767 + }, + { + "days": 4.801389, + "weight": 2.4221762069449073e-05 + }, + { + "days": 4.827083, + "weight": 0.00011983398076464277 + }, + { + "days": 4.884722, + "weight": 0.00020397273321641325 + }, + { + "days": 4.886806, + "weight": 2.2946932486846487e-05 + }, + { + "days": 4.900694, + "weight": 2.804625081725682e-05 + }, + { + "days": 4.940278, + "weight": 2.2946932486846487e-05 + }, + { + "days": 4.942361, + "weight": 5.354284246930848e-05 + }, + { + "days": 4.95, + "weight": 7.776460453875755e-05 + }, + { + "days": 4.951389, + "weight": 0.00020397273321641325 + }, + { + "days": 4.952083, + "weight": 2.2946932486846487e-05 + }, + { + "days": 4.952778, + "weight": 2.2946932486846487e-05 + }, + { + "days": 4.963889, + "weight": 3.314556914766715e-05 + }, + { + "days": 4.970139, + "weight": 0.00013130744700806603 + }, + { + "days": 4.978472, + "weight": 2.804625081725682e-05 + }, + { + "days": 4.9875, + "weight": 5.226801288670589e-05 + }, + { + "days": 4.99375, + "weight": 2.5496591652051656e-05 + }, + { + "days": 5.0, + "weight": 2.1672102904243905e-05 + }, + { + "days": 5.00625, + "weight": 2.677142123465424e-05 + }, + { + "days": 5.007639, + "weight": 3.4420398730269734e-05 + }, + { + "days": 5.011806, + "weight": 3.4420398730269734e-05 + }, + { + "days": 5.016667, + "weight": 3.314556914766715e-05 + }, + { + "days": 5.018056, + "weight": 8.286392286916788e-05 + }, + { + "days": 5.019444, + "weight": 5.481767205191106e-05 + }, + { + "days": 5.027778, + "weight": 2.804625081725682e-05 + }, + { + "days": 5.03125, + "weight": 2.5496591652051656e-05 + }, + { + "days": 5.054861, + "weight": 5.736733121711622e-05 + }, + { + "days": 5.078472, + "weight": 3.9519717060680065e-05 + }, + { + "days": 5.086111, + "weight": 2.804625081725682e-05 + }, + { + "days": 5.095833, + "weight": 3.059590998246198e-05 + }, + { + "days": 5.097917, + "weight": 2.2946932486846487e-05 + }, + { + "days": 5.103472, + "weight": 4.9718353721500726e-05 + }, + { + "days": 5.114583, + "weight": 2.677142123465424e-05 + }, + { + "days": 5.13125, + "weight": 2.5496591652051656e-05 + }, + { + "days": 5.771528, + "weight": 4.9718353721500726e-05 + }, + { + "days": 5.838194, + "weight": 7.39401157909498e-05 + }, + { + "days": 5.891667, + "weight": 3.059590998246198e-05 + }, + { + "days": 5.91875, + "weight": 0.00011728432159943762 + }, + { + "days": 5.936111, + "weight": 3.824488747807748e-05 + }, + { + "days": 5.947917, + "weight": 2.4221762069449073e-05 + }, + { + "days": 5.986111, + "weight": 6.119181996492397e-05 + }, + { + "days": 5.997222, + "weight": 3.059590998246198e-05 + }, + { + "days": 5.998611, + "weight": 2.5496591652051656e-05 + }, + { + "days": 6.002778, + "weight": 3.824488747807748e-05 + }, + { + "days": 6.00625, + "weight": 3.4420398730269734e-05 + }, + { + "days": 6.017361, + "weight": 3.187073956506457e-05 + }, + { + "days": 6.04375, + "weight": 4.4619035391090396e-05 + }, + { + "days": 6.124306, + "weight": 3.059590998246198e-05 + }, + { + "days": 6.765972, + "weight": 2.9321080399859404e-05 + }, + { + "days": 6.844444, + "weight": 5.354284246930848e-05 + }, + { + "days": 6.902083, + "weight": 5.481767205191106e-05 + }, + { + "days": 6.947917, + "weight": 2.5496591652051656e-05 + }, + { + "days": 6.963194, + "weight": 5.736733121711622e-05 + }, + { + "days": 6.970833, + "weight": 2.804625081725682e-05 + }, + { + "days": 7.010417, + "weight": 4.4619035391090396e-05 + }, + { + "days": 7.017361, + "weight": 3.69700578954749e-05 + }, + { + "days": 7.018056, + "weight": 2.2946932486846487e-05 + }, + { + "days": 7.051389, + "weight": 2.677142123465424e-05 + }, + { + "days": 7.965278, + "weight": 2.5496591652051656e-05 + }, + { + "days": 9.828472, + "weight": 7.011562704314205e-05 + }, + { + "days": 9.863889, + "weight": 3.69700578954749e-05 + }, + { + "days": 9.931944, + "weight": 7.266528620834722e-05 + }, + { + "days": 9.985417, + "weight": 3.187073956506457e-05 + }, + { + "days": 10.048611, + "weight": 2.9321080399859404e-05 + }, + { + "days": 10.845833, + "weight": 2.804625081725682e-05 + }, + { + "days": 10.914583, + "weight": 2.4221762069449073e-05 + }, + { + "days": 11.076389, + "weight": 4.9718353721500726e-05 + }, + { + "days": 11.150694, + "weight": 2.4221762069449073e-05 + }, + { + "days": 11.209722, + "weight": 3.824488747807748e-05 + }, + { + "days": 11.26875, + "weight": 3.4420398730269734e-05 + }, + { + "days": 11.844444, + "weight": 2.9321080399859404e-05 + }, + { + "days": 11.904861, + "weight": 2.804625081725682e-05 + }, + { + "days": 12.906944, + "weight": 3.059590998246198e-05 + }, + { + "days": 16.880556, + "weight": 3.187073956506457e-05 + }, + { + "days": 17.958333, + "weight": 3.824488747807748e-05 + }, + { + "days": 22.287131, + "weight": 0.012499999999999999 + }, + { + "days": 25.1029, + "weight": 0.012499999999999999 + }, + { + "days": 28.274415, + "weight": 0.012499999999999999 + }, + { + "days": 31.846621, + "weight": 0.012499999999999999 + }, + { + "days": 35.870141, + "weight": 0.012499999999999999 + }, + { + "days": 40.401996, + "weight": 0.012499999999999999 + }, + { + "days": 45.506408, + "weight": 0.012499999999999999 + }, + { + "days": 51.255714, + "weight": 0.012499999999999999 + }, + { + "days": 57.73139, + "weight": 0.012499999999999999 + }, + { + "days": 65.025208, + "weight": 0.012499999999999999 + }, + { + "days": 73.240531, + "weight": 0.012499999999999999 + }, + { + "days": 82.493782, + "weight": 0.012499999999999999 + }, + { + "days": 92.916094, + "weight": 0.012499999999999999 + }, + { + "days": 104.655168, + "weight": 0.012499999999999999 + }, + { + "days": 117.877362, + "weight": 0.012499999999999999 + }, + { + "days": 132.770057, + "weight": 0.012499999999999999 + }, + { + "days": 149.544303, + "weight": 0.012499999999999999 + }, + { + "days": 168.437818, + "weight": 0.012499999999999999 + }, + { + "days": 189.71835, + "weight": 0.012499999999999999 + }, + { + "days": 213.687476, + "weight": 0.012499999999999999 + }, + { + "days": 240.684877, + "weight": 0.012499999999999999 + }, + { + "days": 271.093144, + "weight": 0.012499999999999999 + }, + { + "days": 305.34321, + "weight": 0.012499999999999999 + }, + { + "days": 343.920448, + "weight": 0.012499999999999999 + }, + { + "new_client": true, + "weight": 0.015466232496134534 + } + ] +} \ No newline at end of file diff --git a/tools/DeltaIndexTestTool/make_distribution.py b/tools/DeltaIndexTestTool/make_distribution.py new file mode 100644 index 0000000000..042152310e --- /dev/null +++ b/tools/DeltaIndexTestTool/make_distribution.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +""" +make_distribution.py - Convert a telemetry export into an analyze.py distribution JSON. + +Input is a CSV export of update-check events with the columns: + + "series name","x","y" + +where `x` is the age in days of the client's previous index version at the moment +of the event, and `y` is the number of events observed at that age. An `x` of -1 +means the client had no previous version, i.e. a net-new client that must download +a full baseline regardless of schedule. + +Because each row already counts *events*, the resulting weights are a distribution +over download events -- exactly what analyze.py's cost model expects. No additional +weighting by update frequency is applied. + +Output is the JSON format analyze.py consumes: + + { + "description": "...", + "buckets": [ + { "days": 0.5, "weight": 0.21 }, + { "new_client": true, "weight": 0.022 } + ] + } + +TRUNCATED EXPORTS +----------------- +Exports that keep only the top N buckets by event count systematically discard the +stale tail. Ages are floating-point differences between individual index publications +(a few hours apart), so frequent updaters pile onto a handful of shared ages while a +client returning after 40 days lands on a nearly unique age. Those rare ages fall +below the export's cutoff and disappear entirely -- not because such clients are rare, +but because their events are spread thin. + +The symptom is a sharp floor in the per-bucket counts: no bucket has fewer than some +value, and the bucket count is a round number. --estimate-tail detects that floor, +fits a power law to the age bands that sit comfortably above it, and reinstates the +predicted missing mass as synthetic tail buckets. + +Note for interpretation: any client staler than the refresh period must take a full +baseline, exactly like a net-new client. Missing tail mass therefore acts as a +roughly constant tax on every candidate period -- it lowers the predicted savings +without greatly moving the optimal interval. + +Usage: + python make_distribution.py --csv export.csv --output distribution.json + python make_distribution.py --csv export.csv --output distribution.json --estimate-tail + python make_distribution.py --csv export.csv --output distribution.json --tail-fraction 0.15 +""" + +import argparse +import csv +import json +import math +import sys +from collections import defaultdict + + +def load_events(csv_path): + """Return (staleness_events, new_client_count). + + staleness_events is a list of (days, count) with days >= 0. + """ + staleness = [] + new_clients = 0.0 + with open(csv_path, newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f): + try: + x = float(row["x"]) + y = float(row["y"]) + except (KeyError, TypeError, ValueError): + continue + if y <= 0: + continue + if x < 0: + new_clients += y + else: + staleness.append((x, y)) + return staleness, new_clients + + +def detect_truncation(staleness): + """Return (floor, is_truncated) for the export's per-bucket count floor. + + A 'top N buckets' export leaves a hard floor: the smallest surviving bucket + count sits well above 1, and many buckets cluster just above it. + """ + if not staleness: + return 0.0, False + counts = sorted(c for _, c in staleness) + floor = counts[0] + near_floor = sum(1 for c in counts if c < floor * 1.5) + return floor, floor > 5 and near_floor >= 0.05 * len(counts) + + +def estimate_tail(staleness, floor, max_days, clean_factor=4.0): + """Estimate mass lost to top-N truncation and return it as (days, count) buckets. + + Fits density(D) = C * D**k over geometric age bands whose mean bucket count is at + least `clean_factor` times the truncation floor -- those bands are essentially + unaffected by the cut -- then extrapolates out to `max_days` and reinstates the + difference between predicted and observed mass. + """ + if not staleness: + return [], None + + edges = [] + e = 0.25 + while e < max_days: + edges.append(e) + e *= 1.5 + edges.append(max_days) + + bands = [] + for lo, hi in zip(edges, edges[1:]): + sel = [c for d, c in staleness if lo <= d < hi] + if sel: + bands.append((math.sqrt(lo * hi), sum(sel) / (hi - lo), sum(sel) / len(sel))) + + clean = [(mid, dens) for mid, dens, mean_count in bands if mean_count >= clean_factor * floor] + if len(clean) < 3: + return [], None + + n = len(clean) + sx = sum(math.log(d) for d, _ in clean) + sy = sum(math.log(v) for _, v in clean) + sxx = sum(math.log(d) ** 2 for d, _ in clean) + sxy = sum(math.log(d) * math.log(v) for d, v in clean) + denom = n * sxx - sx * sx + if abs(denom) < 1e-12: + return [], None + k = (n * sxy - sx * sy) / denom + c = math.exp((sy - k * sx) / n) + if k >= -1.0: + # Flatter than 1/D: the integral diverges and the fit cannot be trusted + # to extrapolate. Refuse rather than invent an enormous tail. + return [], (c, k) + + def predicted(a, b): + return c * (b ** (k + 1) - a ** (k + 1)) / (k + 1) + + start = max(mid for mid, _, _ in bands if mid <= max(m for m, _ in clean)) + tail = [] + lo = start + while lo < max_days: + hi = min(lo * 1.5, max_days) + observed = sum(cnt for d, cnt in staleness if lo <= d < hi) + missing = predicted(lo, hi) - observed + if missing > 0: + tail.append((math.sqrt(lo * hi), missing)) + lo = hi + return tail, (c, k) + + +def spread_tail(mass, min_days, max_days, steps=24): + """Distribute `mass` events log-uniformly across [min_days, max_days]. + + Spreading matters: concentrating the assumed tail at a single age makes every + refresh period shorter than that age pay the full cost and every longer period + amortize it, which manufactures a spurious optimum right at the chosen age. + """ + if mass <= 0 or max_days <= min_days: + return [] + ratio = (max_days / min_days) ** (1.0 / steps) + edges = [min_days * ratio ** i for i in range(steps + 1)] + per = mass / steps + return [(math.sqrt(lo * hi), per) for lo, hi in zip(edges, edges[1:])] + + +def bin_events(staleness, bin_days): + """Aggregate (days, count) pairs into bins. + + Each bin is represented by its count-weighted mean age, which keeps the + expectation of min(D, P) exact for every bin that does not straddle P. + A bin_days of 0 disables binning and keeps every distinct age. + """ + if bin_days <= 0: + merged = defaultdict(float) + for days, count in staleness: + merged[days] += count + return sorted(merged.items()) + + sums = defaultdict(float) + counts = defaultdict(float) + for days, count in staleness: + key = int(days / bin_days) + sums[key] += days * count + counts[key] += count + return [(sums[k] / counts[k], counts[k]) for k in sorted(counts)] + + +def main(): + parser = argparse.ArgumentParser( + description="Convert a telemetry export into an analyze.py distribution JSON.") + parser.add_argument("--csv", required=True, help="Telemetry export CSV path") + parser.add_argument("--output", required=True, help="Destination JSON path") + parser.add_argument("--bin-days", type=float, default=0.0, dest="bin_days", + help="Bin width in days for aggregating ages. " + "0 (default) keeps every distinct age, which is exact.") + parser.add_argument("--description", default=None, + help="Description recorded in the JSON. Defaults to a generated one.") + parser.add_argument("--estimate-tail", action="store_true", dest="estimate_tail", + help="Detect top-N truncation and reinstate the missing stale tail " + "by power-law extrapolation. See the module docstring.") + parser.add_argument("--tail-fraction", type=float, default=None, dest="tail_fraction", + help="Instead of estimating, assert that this fraction (0-1) of all " + "events are staler than the export shows. Overrides --estimate-tail.") + parser.add_argument("--tail-min-days", type=float, default=21.0, dest="tail_min_days", + help="Youngest age for --tail-fraction mass. Default 21.") + parser.add_argument("--tail-max-days", type=float, default=365.0, dest="tail_max_days", + help="Upper age limit when extrapolating with --estimate-tail. Default 365.") + args = parser.parse_args() + + staleness, new_clients = load_events(args.csv) + if not staleness and not new_clients: + print("Error: no usable events found in CSV.", file=sys.stderr) + sys.exit(1) + + floor, truncated = detect_truncation(staleness) + observed_total = sum(c for _, c in staleness) + new_clients + + tail = [] + fit = None + if args.tail_fraction is not None: + if not 0.0 <= args.tail_fraction < 1.0: + print("Error: --tail-fraction must be in [0, 1).", file=sys.stderr) + sys.exit(1) + if args.tail_fraction > 0: + # Mass m such that m / (observed + m) == tail_fraction. + mass = observed_total * args.tail_fraction / (1.0 - args.tail_fraction) + # Spread log-uniformly rather than lumping at one age: a point mass creates + # an artificial cliff at that age, because periods longer than it suddenly + # start amortizing those clients and a spurious second optimum appears. + tail = spread_tail(mass, args.tail_min_days, args.tail_max_days) + elif args.estimate_tail: + tail, fit = estimate_tail(staleness, floor, args.tail_max_days) + if not tail: + print("Warning: could not fit a usable tail; emitting the observed data unchanged.", + file=sys.stderr) + + binned = bin_events(staleness, args.bin_days) + [(d, c) for d, c in tail] + binned.sort() + total = sum(c for _, c in binned) + new_clients + + buckets = [{"days": round(days, 6), "weight": count / total} for days, count in binned] + if new_clients > 0: + buckets.append({"new_client": True, "weight": new_clients / total}) + + max_age = max((d for d, _ in binned), default=0.0) + tail_mass = sum(c for _, c in tail) + description = args.description or ( + f"Telemetry-derived from {args.csv}: {total:,.0f} download events, " + f"{100.0 * new_clients / total:.2f}% net-new clients, " + f"observed ages 0-{max(d for d, _ in staleness):.1f} days" + + (f", plus {100.0 * tail_mass / total:.1f}% reinstated stale tail out to " + f"{max_age:.0f} days" if tail_mass else "")) + + with open(args.output, "w", encoding="utf-8") as f: + json.dump({"description": description, "buckets": buckets}, f, indent=2) + + mean_age = sum(d * c for d, c in binned) / sum(c for _, c in binned) if binned else 0.0 + print(f"Wrote {args.output}") + print(f" Events: {total:,.0f}") + print(f" Buckets: {len(buckets)}") + print(f" New clients: {100.0 * new_clients / total:.2f}%") + print(f" Mean age: {mean_age:.3f} days") + print(f" Max age: {max_age:.2f} days") + if truncated: + print(f" NOTE: export looks truncated -- no bucket below {floor:.0f} events. " + f"The stale tail is under-counted.") + if not tail_mass: + print(f" Re-run with --estimate-tail or --tail-fraction to model it.") + if fit: + print(f" Tail fit: density = {fit[0]:.0f} * D^{fit[1]:.3f}") + if tail_mass: + print(f" Tail added: {tail_mass:,.0f} events ({100.0 * tail_mass / total:.2f}% of total)") + + +if __name__ == "__main__": + main() From 0aecc287f4c958101a9d0d22d917da997ce3b91b Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Wed, 2 Sep 2026 11:08:29 -0700 Subject: [PATCH 14/36] Remove extra portions of delta poc --- .../SQLiteStatementBuilder.cpp | 6 +- src/WinGetUtil/Exports.cpp | 18 - src/WinGetUtil/WinGetUtil.h | 8 - src/WinGetUtilInterop/Api/WinGetFactory.cs | 24 - .../Interfaces/IWinGetFactory.cs | 8 - .../Interfaces/IWinGetSQLiteIndex.cs | 12 - .../DeltaIndexTestTool.csproj | 40 - .../DeltaIndexTestTool/DeltaIndexTestTool.sln | 47 - tools/DeltaIndexTestTool/Program.cs | 1007 --- tools/DeltaIndexTestTool/analyze.py | 491 -- .../DeltaIndexTestTool/baseline-analysis.png | Bin 136103 -> 0 bytes tools/DeltaIndexTestTool/dist-observed.json | 7205 ---------------- tools/DeltaIndexTestTool/dist-tail-10.json | 7301 ----------------- tools/DeltaIndexTestTool/dist-tail-20.json | 7301 ----------------- tools/DeltaIndexTestTool/dist-tail-30.json | 7301 ----------------- tools/DeltaIndexTestTool/make_distribution.py | 284 - 16 files changed, 4 insertions(+), 31049 deletions(-) delete mode 100644 tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj delete mode 100644 tools/DeltaIndexTestTool/DeltaIndexTestTool.sln delete mode 100644 tools/DeltaIndexTestTool/Program.cs delete mode 100644 tools/DeltaIndexTestTool/analyze.py delete mode 100644 tools/DeltaIndexTestTool/baseline-analysis.png delete mode 100644 tools/DeltaIndexTestTool/dist-observed.json delete mode 100644 tools/DeltaIndexTestTool/dist-tail-10.json delete mode 100644 tools/DeltaIndexTestTool/dist-tail-20.json delete mode 100644 tools/DeltaIndexTestTool/dist-tail-30.json delete mode 100644 tools/DeltaIndexTestTool/make_distribution.py diff --git a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp index fb4b0f55b3..4277ebf1b5 100644 --- a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp +++ b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp @@ -346,8 +346,10 @@ namespace AppInstaller::SQLite::Builder StatementBuilder& StatementBuilder::Equals(std::nullptr_t) { - m_stream << " = NULL"; - return *this; + // This is almost certainly not what you want. + // In SQL, value = NULL is always false. + // Use StatementBuilder::IsNull instead. + THROW_HR(E_NOTIMPL); } StatementBuilder& StatementBuilder::Equals() diff --git a/src/WinGetUtil/Exports.cpp b/src/WinGetUtil/Exports.cpp index ae37aaf8a7..1330731ff8 100644 --- a/src/WinGetUtil/Exports.cpp +++ b/src/WinGetUtil/Exports.cpp @@ -35,8 +35,6 @@ namespace { case WinGetSQLiteIndexProperty_PackageUpdateTrackingBaseTime: return SQLiteIndex::Property::PackageUpdateTrackingBaseTime; case WinGetSQLiteIndexProperty_IntermediateFileOutputPath: return SQLiteIndex::Property::IntermediateFileOutputPath; - case WinGetSQLiteIndexProperty_DeltaBaselineIndexPath: return SQLiteIndex::Property::DeltaBaselineIndexPath; - case WinGetSQLiteIndexProperty_DeltaOutputPath: return SQLiteIndex::Property::DeltaOutputPath; } THROW_HR(E_INVALIDARG); @@ -124,22 +122,6 @@ extern "C" } CATCH_RETURN() - WINGET_UTIL_API WinGetSQLiteIndexOpenWithBaseline(WINGET_STRING deltaFilePath, WINGET_STRING baselineFilePath, WINGET_SQLITE_INDEX_HANDLE* index) try - { - THROW_HR_IF(E_INVALIDARG, !deltaFilePath); - THROW_HR_IF(E_INVALIDARG, !baselineFilePath); - THROW_HR_IF(E_INVALIDARG, !index); - THROW_HR_IF(E_INVALIDARG, !!*index); - - std::unique_ptr result = std::make_unique( - SQLiteIndex::OpenWithBaseline(ConvertToUTF8(deltaFilePath), ConvertToUTF8(baselineFilePath))); - - *index = static_cast(result.release()); - - return S_OK; - } - CATCH_RETURN() - WINGET_UTIL_API WinGetSQLiteIndexClose(WINGET_SQLITE_INDEX_HANDLE index) try { std::unique_ptr toClose(reinterpret_cast(index)); diff --git a/src/WinGetUtil/WinGetUtil.h b/src/WinGetUtil/WinGetUtil.h index 5f0bc1491e..a089d600ef 100644 --- a/src/WinGetUtil/WinGetUtil.h +++ b/src/WinGetUtil/WinGetUtil.h @@ -129,12 +129,6 @@ extern "C" WINGET_STRING filePath, WINGET_SQLITE_INDEX_HANDLE* index); - // Opens an existing delta index combined with its baseline for reading. - WINGET_UTIL_API WinGetSQLiteIndexOpenWithBaseline( - WINGET_STRING deltaFilePath, - WINGET_STRING baselineFilePath, - WINGET_SQLITE_INDEX_HANDLE* index); - // Closes the index. WINGET_UTIL_API WinGetSQLiteIndexClose( WINGET_SQLITE_INDEX_HANDLE index); @@ -149,8 +143,6 @@ extern "C" { WinGetSQLiteIndexProperty_PackageUpdateTrackingBaseTime = 0, WinGetSQLiteIndexProperty_IntermediateFileOutputPath = 1, - WinGetSQLiteIndexProperty_DeltaBaselineIndexPath = 2, - WinGetSQLiteIndexProperty_DeltaOutputPath = 3, }; // Sets the given property on the index. diff --git a/src/WinGetUtilInterop/Api/WinGetFactory.cs b/src/WinGetUtilInterop/Api/WinGetFactory.cs index b99a4c7304..7486daf00a 100644 --- a/src/WinGetUtilInterop/Api/WinGetFactory.cs +++ b/src/WinGetUtilInterop/Api/WinGetFactory.cs @@ -59,20 +59,6 @@ public IWinGetSQLiteIndex SQLiteIndexOpen(string indexFile) } } - /// - public IWinGetSQLiteIndex SQLiteIndexOpenWithBaseline(string deltaIndexFile, string baselineIndexFile) - { - try - { - WinGetSQLiteIndexOpenWithBaseline(deltaIndexFile, baselineIndexFile, out IntPtr index); - return new WinGetSQLiteIndex(index); - } - catch (Exception e) - { - throw new WinGetSQLiteIndexException(e); - } - } - /// public IWinGetLogging LoggingInit(string indexLogFile) { @@ -231,16 +217,6 @@ private static CreateManifestResult ParseJsonManifestResult(bool succeeded, stri [DllImport(Constants.DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] private static extern IntPtr WinGetSQLiteIndexOpen(string filePath, out IntPtr index); - /// - /// Opens an existing delta index combined with its baseline for reading. - /// - /// File path of delta index. - /// File path of baseline index. - /// Out handle of the index. - /// HRESULT. - [DllImport(Constants.DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, PreserveSig = false)] - private static extern IntPtr WinGetSQLiteIndexOpenWithBaseline(string deltaFilePath, string baselineFilePath, out IntPtr index); - /// /// Initializes the logging infrastructure. /// diff --git a/src/WinGetUtilInterop/Interfaces/IWinGetFactory.cs b/src/WinGetUtilInterop/Interfaces/IWinGetFactory.cs index 401719b3f6..8e450ca5de 100644 --- a/src/WinGetUtilInterop/Interfaces/IWinGetFactory.cs +++ b/src/WinGetUtilInterop/Interfaces/IWinGetFactory.cs @@ -36,14 +36,6 @@ public interface IWinGetFactory /// Instance of IWinGetSQLiteIndex. IWinGetSQLiteIndex SQLiteIndexOpen(string indexFile); - /// - /// Opens a delta index combined with its baseline for reading. - /// - /// Delta index file to open. - /// Baseline index file to attach. - /// Instance of IWinGetSQLiteIndex. - IWinGetSQLiteIndex SQLiteIndexOpenWithBaseline(string deltaIndexFile, string baselineIndexFile); - /// /// Initializes logging. /// diff --git a/src/WinGetUtilInterop/Interfaces/IWinGetSQLiteIndex.cs b/src/WinGetUtilInterop/Interfaces/IWinGetSQLiteIndex.cs index 64759c5156..b819e6fcc5 100644 --- a/src/WinGetUtilInterop/Interfaces/IWinGetSQLiteIndex.cs +++ b/src/WinGetUtilInterop/Interfaces/IWinGetSQLiteIndex.cs @@ -26,18 +26,6 @@ public enum SQLiteIndexProperty /// The path does not need to exist, and may not be created if no files need to be written. /// IntermediateFileOutputPath = 1, - - /// - /// The full path to the baseline V2 index file to compare against when generating a delta. - /// Must be set together with DeltaOutputPath before calling PrepareForPackaging. - /// - DeltaBaselineIndexPath = 2, - - /// - /// The full path where the delta index file will be written. - /// Must be set together with DeltaBaselineIndexPath before calling PrepareForPackaging. - /// - DeltaOutputPath = 3, } /// diff --git a/tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj b/tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj deleted file mode 100644 index ca1f6c32b0..0000000000 --- a/tools/DeltaIndexTestTool/DeltaIndexTestTool.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - Exe - net8.0 - x64;x86 - enable - enable - - $(MSBuildProjectDirectory)\bin\$(Platform)\$(Configuration)\ - - $(MSBuildThisFileDirectory)..\..\src\$(Platform)\$(Configuration)\WinGetUtil\ - - - - - - - - - - - - - - - - - diff --git a/tools/DeltaIndexTestTool/DeltaIndexTestTool.sln b/tools/DeltaIndexTestTool/DeltaIndexTestTool.sln deleted file mode 100644 index 0c73aeeacb..0000000000 --- a/tools/DeltaIndexTestTool/DeltaIndexTestTool.sln +++ /dev/null @@ -1,47 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.3.11512.155 d18.3 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeltaIndexTestTool", "DeltaIndexTestTool.csproj", "{31C921DB-7C37-4303-894C-A33A3951562B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinGetUtilInterop", "..\..\src\WinGetUtilInterop\WinGetUtilInterop.csproj", "{C09374FD-7E4A-4FD5-9BDE-16E84D38F731}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|Any CPU.ActiveCfg = Debug|x64 - {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|Any CPU.Build.0 = Debug|x64 - {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x64.ActiveCfg = Debug|x64 - {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x64.Build.0 = Debug|x64 - {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x86.ActiveCfg = Debug|x86 - {31C921DB-7C37-4303-894C-A33A3951562B}.Debug|x86.Build.0 = Debug|x86 - {31C921DB-7C37-4303-894C-A33A3951562B}.Release|Any CPU.ActiveCfg = Release|x64 - {31C921DB-7C37-4303-894C-A33A3951562B}.Release|Any CPU.Build.0 = Release|x64 - {31C921DB-7C37-4303-894C-A33A3951562B}.Release|x64.ActiveCfg = Release|x64 - {31C921DB-7C37-4303-894C-A33A3951562B}.Release|x64.Build.0 = Release|x64 - {31C921DB-7C37-4303-894C-A33A3951562B}.Release|x86.ActiveCfg = Release|x86 - {31C921DB-7C37-4303-894C-A33A3951562B}.Release|x86.Build.0 = Release|x86 - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x64.ActiveCfg = Debug|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x64.Build.0 = Debug|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x86.ActiveCfg = Debug|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Debug|x86.Build.0 = Debug|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|Any CPU.Build.0 = Release|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x64.ActiveCfg = Release|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x64.Build.0 = Release|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x86.ActiveCfg = Release|Any CPU - {C09374FD-7E4A-4FD5-9BDE-16E84D38F731}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/tools/DeltaIndexTestTool/Program.cs b/tools/DeltaIndexTestTool/Program.cs deleted file mode 100644 index 51bf508652..0000000000 --- a/tools/DeltaIndexTestTool/Program.cs +++ /dev/null @@ -1,1007 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace DeltaIndexTestTool -{ - using LibGit2Sharp; - using Microsoft.WinGetUtil.Api; - using Microsoft.WinGetUtil.Interfaces; - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.IO; - using System.IO.Compression; - using System.Linq; - using System.Text; - using System.Text.Json; - using System.Text.Json.Serialization; - - /// - /// Walks the git history of a winget-pkgs clone at weekly intervals, building a full V2 index - /// and a delta index at each checkpoint, then reports the cumulative download size comparison - /// between always-downloading-full vs downloading-baseline-plus-deltas strategies. - /// - class Program - { - static int Main(string[] args) - { - string repoPath = string.Empty; - string outputDir = string.Empty; - int intervalDays = 7; - int maxCheckpoints = 0; - string branch = "master"; - string resumeCommit = string.Empty; - string resumeWorkingIndexPath = string.Empty; - bool autoResume = false; - bool recomputeCompressed = false; - - for (int i = 0; i < args.Length; i++) - { - switch (args[i]) - { - case "--repo" when i + 1 < args.Length: - repoPath = args[++i]; - break; - case "--output" when i + 1 < args.Length: - outputDir = args[++i]; - break; - case "--interval" when i + 1 < args.Length: - intervalDays = int.Parse(args[++i]); - break; - case "--max" when i + 1 < args.Length: - maxCheckpoints = int.Parse(args[++i]); - break; - case "--branch" when i + 1 < args.Length: - branch = args[++i]; - break; - case "--resume-commit" when i + 1 < args.Length: - resumeCommit = args[++i]; - break; - case "--resume-working-index" when i + 1 < args.Length: - resumeWorkingIndexPath = args[++i]; - break; - case "--resume": - autoResume = true; - break; - case "--recompute-compressed": - recomputeCompressed = true; - break; - case "--help": - case "-h": - PrintUsage(); - return 0; - default: - PrintUsage(); - return 1; - } - } - - if (string.IsNullOrEmpty(outputDir)) - { - PrintUsage(); - return 1; - } - - if (!autoResume) - { - if (string.IsNullOrEmpty(repoPath)) - { - PrintUsage(); - return 1; - } - - if (!Directory.Exists(repoPath)) - { - Console.Error.WriteLine($"Repository path does not exist: {repoPath}"); - return 1; - } - } - - // --resume-working-index requires --resume-commit; the reverse is fine (build from scratch at that commit) - if (!string.IsNullOrEmpty(resumeWorkingIndexPath) && string.IsNullOrEmpty(resumeCommit)) - { - Console.Error.WriteLine("--resume-working-index requires --resume-commit."); - return 1; - } - if (!string.IsNullOrEmpty(resumeWorkingIndexPath) && !File.Exists(resumeWorkingIndexPath)) - { - Console.Error.WriteLine($"Resume working index not found: {resumeWorkingIndexPath}"); - return 1; - } - if (autoResume && (!string.IsNullOrEmpty(resumeCommit) || !string.IsNullOrEmpty(resumeWorkingIndexPath))) - { - Console.Error.WriteLine("--resume cannot be combined with --resume-commit or --resume-working-index."); - return 1; - } - if ((autoResume || recomputeCompressed) && !File.Exists(Path.Combine(outputDir, "state.json"))) - { - Console.Error.WriteLine($"No state.json found in output directory. Run without --resume first."); - return 1; - } - if (recomputeCompressed && !autoResume) - { - Console.Error.WriteLine("--recompute-compressed requires --resume."); - return 1; - } - - Directory.CreateDirectory(outputDir); - - try - { - RunAnalysis(repoPath, outputDir, branch, intervalDays, maxCheckpoints, - resumeCommit, resumeWorkingIndexPath, autoResume, recomputeCompressed); - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - Console.Error.WriteLine(ex.StackTrace); - return 1; - } - } - - static void PrintUsage() - { - Console.WriteLine("DeltaIndexTestTool - Measures delta index size vs full index size over git history"); - Console.WriteLine(); - Console.WriteLine("Usage: DeltaIndexTestTool --repo --output [options]"); - Console.WriteLine(); - Console.WriteLine("Options:"); - Console.WriteLine(" --repo Path to local winget-pkgs git clone"); - Console.WriteLine(" --output Directory to write results and index files"); - Console.WriteLine(" --interval Interval between checkpoints in days (default: 7)"); - Console.WriteLine(" --max Maximum number of checkpoints; selects the N most"); - Console.WriteLine(" recent intervals working backward from HEAD"); - Console.WriteLine(" --branch Branch to walk (default: master)"); - Console.WriteLine(" --resume Resume from the last complete checkpoint recorded in"); - Console.WriteLine(" state.json (requires prior run with same --output dir)."); - Console.WriteLine(" Cannot be combined with --resume-commit."); - Console.WriteLine(" --recompute-compressed Used with --resume: recomputes compressed file sizes for"); - Console.WriteLine(" all already-completed checkpoints before continuing."); - Console.WriteLine(" Use this to backfill compressed sizes into a run that"); - Console.WriteLine(" completed before compression measurement was added."); - Console.WriteLine(" --resume-commit Commit SHA to resume from"); - Console.WriteLine(" --resume-working-index Path to pre-packaging working index for resume commit"); - Console.WriteLine(); - Console.WriteLine("Resume modes:"); - Console.WriteLine(" --resume Reads state.json from the output directory, finds the"); - Console.WriteLine(" first incomplete checkpoint, and continues from there."); - Console.WriteLine(" --resume-commit only Starts a fresh index at that commit, then continues"); - Console.WriteLine(" forward from there (skips re-walking older history)."); - Console.WriteLine(" --resume-commit + --resume-working-index"); - Console.WriteLine(" Uses the provided pre-built working index as checkpoint 0,"); - Console.WriteLine(" packages it, then continues with subsequent checkpoints."); - Console.WriteLine(" --resume-working-index requires --resume-commit."); - Console.WriteLine(); - Console.WriteLine("Output:"); - Console.WriteLine(" state.json Run parameters and last complete checkpoint index (auto-managed)"); - Console.WriteLine(" results.csv CSV of checkpoint sizes"); - } - - static void RunAnalysis(string repoPath, string outputDir, string branch, int intervalDays, int maxCheckpoints, - string resumeCommit, string resumeWorkingIndexPath, bool autoResume, bool recomputeCompressed) - { - Console.WriteLine($"Output directory: {outputDir}"); - - string stateFilePath = Path.Combine(outputDir, "state.json"); - bool hasResumeCommit = !string.IsNullOrEmpty(resumeCommit); - - List checkpoints; - ToolState state; - - if (autoResume) - { - state = LoadState(stateFilePath); - checkpoints = state.Checkpoints.Select(c => new CommitCheckpoint(c.Sha, c.Date)).ToList(); - int startIndex = state.LastCompleteIndex + 1; - - Console.WriteLine($"Opening repository at: {state.RepoPath}"); - Console.WriteLine($"Interval: every {state.IntervalDays} day(s)"); - Console.WriteLine($"Checkpoints: {checkpoints.Count} total, resuming from {startIndex}"); - - if (recomputeCompressed && startIndex > 0) - { - Console.WriteLine($"\nRecomputing compressed sizes for {startIndex} completed checkpoint(s)..."); - for (int ci = 0; ci < startIndex; ci++) - { - string cpDir = Path.Combine(outputDir, $"checkpoint_{ci:D4}"); - string fullPath = Path.Combine(cpDir, "full_index.db"); - string prevPath = Path.Combine(cpDir, "delta_prev.db"); - string origPath = Path.Combine(cpDir, "delta_orig.db"); - - var rec = state.Checkpoints[ci]; - rec.FullIndexCompressedBytes = File.Exists(fullPath) ? GetCompressedSize(fullPath) : 0; - rec.DeltaPrevCompressedBytes = File.Exists(prevPath) ? GetCompressedSize(prevPath) : 0; - rec.DeltaOrigCompressedBytes = File.Exists(origPath) ? GetCompressedSize(origPath) : 0; - - Console.WriteLine($" [{ci + 1}/{startIndex}] checkpoint_{ci:D4}: " + - $"full={rec.FullIndexCompressedBytes / 1024.0 / 1024.0:F2} MB " + - $"prev={rec.DeltaPrevCompressedBytes / 1024.0 / 1024.0:F2} MB " + - $"orig={rec.DeltaOrigCompressedBytes / 1024.0 / 1024.0:F2} MB"); - } - SaveState(state); - Console.WriteLine(" Compressed sizes saved to state.json"); - } - - if (startIndex >= checkpoints.Count) - { - Console.WriteLine("All checkpoints are already complete. Nothing to do."); - // Re-write the CSV with all existing results so the output is consistent. - var allResults = ReconstructResults(outputDir, state.Checkpoints); - WriteCsv(allResults, Path.Combine(outputDir, "results.csv")); - Console.WriteLine($"Results written to: {Path.Combine(outputDir, "results.csv")}"); - return; - } - } - else - { - Console.WriteLine($"Opening repository at: {repoPath}"); - Console.WriteLine($"Interval: every {intervalDays} day(s)"); - - checkpoints = SelectCheckpoints(repoPath, branch, intervalDays, maxCheckpoints, - hasResumeCommit ? resumeCommit : null); - - if (hasResumeCommit) - { - var resumeDate = LookupCommitDate(repoPath, resumeCommit); - checkpoints.Insert(0, new CommitCheckpoint(resumeCommit, resumeDate)); - } - - if (checkpoints.Count == 0) - { - Console.Error.WriteLine("No checkpoints found."); - return; - } - - state = new ToolState - { - StateFilePath = stateFilePath, - RepoPath = repoPath, - Branch = branch, - IntervalDays = intervalDays, - Checkpoints = checkpoints.Select(c => new CheckpointRecord { Sha = c.Sha, Date = c.Date }).ToList(), - LastCompleteIndex = -1, - }; - SaveState(state); - } - - RunAnalysis(state, outputDir, resumeCommit, resumeWorkingIndexPath, checkpoints); - } - - static void RunAnalysis(ToolState state, string outputDir, string resumeCommit, string resumeWorkingIndexPath, List checkpoints) - { - Console.WriteLine($"Selected {checkpoints.Count} checkpoints"); - - string workingIndexPath = Path.Combine(outputDir, "working_index.db"); - var results = new List(); - var factory = new WinGetFactory(); - IWinGetSQLiteIndex? workingIndex = null; - int startIndex = state.LastCompleteIndex + 1; - bool hasResumeIndex = !string.IsNullOrEmpty(resumeWorkingIndexPath); - - // Pre-populate results for already-complete checkpoints. - if (startIndex > 0) - { - results.AddRange(ReconstructResults(outputDir, state.Checkpoints.Take(startIndex))); - } - - // Open the working index from the last complete checkpoint when resuming mid-run. - if (startIndex > 0) - { - string prevSavedPath = Path.Combine(outputDir, $"checkpoint_{startIndex - 1:D4}", "working_index.db"); - Console.WriteLine($"\nCopying working index from checkpoint {startIndex - 1}..."); - File.Copy(prevSavedPath, workingIndexPath, overwrite: true); - workingIndex = factory.SQLiteIndexOpen(workingIndexPath); - } - - try - { - for (int i = startIndex; i < checkpoints.Count; i++) - { - var checkpoint = checkpoints[i]; - Console.WriteLine($"\n[{i + 1}/{checkpoints.Count}] Processing checkpoint: {checkpoint.Sha[..8]} ({checkpoint.Date:yyyy-MM-dd})"); - - var result = new CheckpointResult - { - Index = i, - Date = checkpoint.Date, - CommitSha = checkpoint.Sha[..8], - }; - - string checkpointDir = Path.Combine(outputDir, $"checkpoint_{i:D4}"); - Directory.CreateDirectory(checkpointDir); - - string fullIndexPath = Path.Combine(checkpointDir, "full_index.db"); - string deltaPrevPath = Path.Combine(checkpointDir, "delta_prev.db"); - string deltaOrigPath = Path.Combine(checkpointDir, "delta_orig.db"); - - if (i == 0 && hasResumeIndex) - { - // Resume with a pre-built working index: copy it into position and package. - Console.WriteLine($" Resuming from provided working index (commit {resumeCommit[..Math.Min(8, resumeCommit.Length)]})"); - - File.Copy(resumeWorkingIndexPath, workingIndexPath, overwrite: true); - - string savedWorkingPath = Path.Combine(checkpointDir, "working_index.db"); - File.Copy(workingIndexPath, savedWorkingPath, overwrite: true); - - File.Copy(workingIndexPath, fullIndexPath, overwrite: true); - using (var packagingIndex = factory.SQLiteIndexOpen(fullIndexPath)) - { - packagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); - packagingIndex.PrepareForPackaging(); - } - - workingIndex = factory.SQLiteIndexOpen(workingIndexPath); - - result.FullIndexBytes = new FileInfo(fullIndexPath).Length; - result.FullIndexCompressedBytes = GetCompressedSize(fullIndexPath); - result.PreviousFullIndexPath = null; - result.FullIndexPath = fullIndexPath; - - Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB ({result.FullIndexCompressedBytes / 1024.0 / 1024.0:F2} MB compressed)"); - } - else if (i == 0) - { - // First checkpoint with no pre-built index: build from scratch. - Console.WriteLine(" Building initial full index from scratch..."); - - if (File.Exists(workingIndexPath)) File.Delete(workingIndexPath); - - workingIndex = factory.SQLiteIndexCreate(workingIndexPath, 2u, 1u); - workingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, "0"); - - int added = AddAllManifests(workingIndex, state.RepoPath, checkpoint.Sha); - Console.WriteLine($" Added {added} manifest files"); - - workingIndex.Dispose(); - workingIndex = null; - - string savedWorkingPath = Path.Combine(checkpointDir, "working_index.db"); - File.Copy(workingIndexPath, savedWorkingPath, overwrite: true); - - File.Copy(workingIndexPath, fullIndexPath, overwrite: true); - using (var packagingIndex = factory.SQLiteIndexOpen(fullIndexPath)) - { - packagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); - packagingIndex.PrepareForPackaging(); - } - - workingIndex = factory.SQLiteIndexOpen(workingIndexPath); - - result.FullIndexBytes = new FileInfo(fullIndexPath).Length; - result.FullIndexCompressedBytes = GetCompressedSize(fullIndexPath); - result.PreviousFullIndexPath = null; - result.FullIndexPath = fullIndexPath; - - Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB ({result.FullIndexCompressedBytes / 1024.0 / 1024.0:F2} MB compressed)"); - } - else - { - // Subsequent checkpoint: apply git diff to working index - var prevCheckpoint = checkpoints[i - 1]; - string prevFullIndexPath = results[i - 1].FullIndexPath!; - string origFullIndexPath = results[0].FullIndexPath!; - - Console.WriteLine(" Applying git diff from previous checkpoint..."); - int changed = ApplyGitDiff(workingIndex!, state.RepoPath, prevCheckpoint.Sha, checkpoint.Sha, checkpointDir); - Console.WriteLine($" Applied {changed} manifest changes"); - - workingIndex!.Dispose(); - workingIndex = null; - - string savedWorkingPath = Path.Combine(checkpointDir, "working_index.db"); - File.Copy(workingIndexPath, savedWorkingPath, overwrite: true); - - // Build full index (no delta properties set) - string fullOnlyPath = fullIndexPath + ".full_only.db"; - File.Copy(workingIndexPath, fullOnlyPath, overwrite: true); - using (var fullPackagingIndex = factory.SQLiteIndexOpen(fullOnlyPath)) - { - fullPackagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); - fullPackagingIndex.PrepareForPackaging(); - } - File.Move(fullOnlyPath, fullIndexPath, overwrite: true); - - // Build delta index against previous full index - string deltaPrevWorkPath = fullIndexPath + ".delta_prev_cp.db"; - File.Copy(workingIndexPath, deltaPrevWorkPath, overwrite: true); - using (var deltaPackagingIndex = factory.SQLiteIndexOpen(deltaPrevWorkPath)) - { - deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaBaselineIndexPath, Path.GetFullPath(prevFullIndexPath)); - deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaOutputPath, Path.GetFullPath(deltaPrevPath)); - deltaPackagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); - deltaPackagingIndex.PrepareForPackaging(); - } - File.Delete(deltaPrevWorkPath); - - // Build delta index against original (checkpoint 0) full index - string deltaOrigWorkPath = fullIndexPath + ".delta_orig_cp.db"; - File.Copy(workingIndexPath, deltaOrigWorkPath, overwrite: true); - using (var deltaPackagingIndex = factory.SQLiteIndexOpen(deltaOrigWorkPath)) - { - deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaBaselineIndexPath, Path.GetFullPath(origFullIndexPath)); - deltaPackagingIndex.SetProperty(SQLiteIndexProperty.DeltaOutputPath, Path.GetFullPath(deltaOrigPath)); - deltaPackagingIndex.SetProperty(SQLiteIndexProperty.PackageUpdateTrackingBaseTime, string.Empty); - deltaPackagingIndex.PrepareForPackaging(); - } - File.Delete(deltaOrigWorkPath); - - workingIndex = factory.SQLiteIndexOpen(workingIndexPath); - - result.FullIndexBytes = new FileInfo(fullIndexPath).Length; - result.DeltaPrevBytes = File.Exists(deltaPrevPath) ? new FileInfo(deltaPrevPath).Length : 0; - result.DeltaOrigBytes = File.Exists(deltaOrigPath) ? new FileInfo(deltaOrigPath).Length : 0; - result.FullIndexCompressedBytes = GetCompressedSize(fullIndexPath); - result.DeltaPrevCompressedBytes = File.Exists(deltaPrevPath) ? GetCompressedSize(deltaPrevPath) : 0; - result.DeltaOrigCompressedBytes = File.Exists(deltaOrigPath) ? GetCompressedSize(deltaOrigPath) : 0; - result.PreviousFullIndexPath = prevFullIndexPath; - result.FullIndexPath = fullIndexPath; - - Console.WriteLine($" Full index: {result.FullIndexBytes / 1024.0 / 1024.0:F2} MB ({result.FullIndexCompressedBytes / 1024.0 / 1024.0:F2} MB compressed)"); - Console.WriteLine($" Delta prev: {result.DeltaPrevBytes / 1024.0 / 1024.0:F2} MB ({result.DeltaPrevCompressedBytes / 1024.0 / 1024.0:F2} MB compressed)"); - Console.WriteLine($" Delta orig: {result.DeltaOrigBytes / 1024.0 / 1024.0:F2} MB ({result.DeltaOrigCompressedBytes / 1024.0 / 1024.0:F2} MB compressed)"); - } - - results.Add(result); - - // Mark this checkpoint complete in the state file, persisting compressed sizes - // so --resume can reconstruct results without re-compressing. - var rec = state.Checkpoints[i]; - rec.FullIndexCompressedBytes = result.FullIndexCompressedBytes; - rec.DeltaPrevCompressedBytes = result.DeltaPrevCompressedBytes; - rec.DeltaOrigCompressedBytes = result.DeltaOrigCompressedBytes; - state.LastCompleteIndex = i; - SaveState(state); - } - } - finally - { - workingIndex?.Dispose(); - } - - string csvPath = Path.Combine(outputDir, "results.csv"); - WriteCsv(results, csvPath); - Console.WriteLine($"\nResults written to: {csvPath}"); - } - - // ----------------------------------------------------------------------- - // State file helpers - // ----------------------------------------------------------------------- - - static readonly JsonSerializerOptions s_jsonOptions = new() - { - WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }; - - static void SaveState(ToolState state) - { - File.WriteAllText(state.StateFilePath, JsonSerializer.Serialize(state, s_jsonOptions), Encoding.UTF8); - } - - static ToolState LoadState(string path) - { - string json = File.ReadAllText(path, Encoding.UTF8); - var result = JsonSerializer.Deserialize(json, s_jsonOptions) - ?? throw new InvalidOperationException($"Failed to deserialize state file: {path}"); - result.StateFilePath = path; - return result; - } - - /// - /// Rebuilds objects for already-complete checkpoints - /// by reading file sizes from disk and compressed sizes from the persisted state records. - /// Used when resuming a run. - /// - static List ReconstructResults(string outputDir, IEnumerable records) - { - var results = new List(); - int i = 0; - foreach (var rec in records) - { - string checkpointDir = Path.Combine(outputDir, $"checkpoint_{i:D4}"); - string fullIndexPath = Path.Combine(checkpointDir, "full_index.db"); - string deltaPrevPath = Path.Combine(checkpointDir, "delta_prev.db"); - string deltaOrigPath = Path.Combine(checkpointDir, "delta_orig.db"); - - results.Add(new CheckpointResult - { - Index = i, - Date = rec.Date, - CommitSha = rec.Sha[..8], - FullIndexBytes = File.Exists(fullIndexPath) ? new FileInfo(fullIndexPath).Length : 0, - DeltaPrevBytes = File.Exists(deltaPrevPath) ? new FileInfo(deltaPrevPath).Length : 0, - DeltaOrigBytes = File.Exists(deltaOrigPath) ? new FileInfo(deltaOrigPath).Length : 0, - FullIndexCompressedBytes = rec.FullIndexCompressedBytes, - DeltaPrevCompressedBytes = rec.DeltaPrevCompressedBytes, - DeltaOrigCompressedBytes = rec.DeltaOrigCompressedBytes, - FullIndexPath = fullIndexPath, - PreviousFullIndexPath = i > 0 - ? Path.Combine(outputDir, $"checkpoint_{i - 1:D4}", "full_index.db") - : null, - }); - i++; - } - return results; - } - - static DateTime LookupCommitDate(string repoPath, string sha) - { - using var repo = new Repository(repoPath); - var commit = repo.Lookup(sha) - ?? throw new InvalidOperationException($"Commit '{sha}' not found in repository"); - return commit.Author.When.DateTime; - } - - /// - /// Selects commits at evenly-spaced intervals across the branch history. - /// - /// When is positive, selects the N most recent - /// intervals working backward from HEAD, then returns them in chronological order. - /// - /// When is provided, only commits strictly after - /// that commit are considered (used for resume mode). - /// - static List SelectCheckpoints(string repoPath, string branch, int intervalDays, int maxCheckpoints, string? afterCommitSha) - { - using var repo = new Repository(repoPath); - - var branchRef = repo.Branches[branch] ?? repo.Branches[$"origin/{branch}"]; - if (branchRef == null) - { - throw new InvalidOperationException($"Branch '{branch}' not found in repository"); - } - - // Resolve the anchor commit's timestamp without loading all commits. - // repo.Lookup handles full and abbreviated SHAs efficiently. - DateTimeOffset? afterTime = null; - if (!string.IsNullOrEmpty(afterCommitSha)) - { - var anchor = repo.Lookup(afterCommitSha); - if (anchor == null) - { - throw new InvalidOperationException($"Resume commit '{afterCommitSha}' not found on branch '{branch}'"); - } - afterTime = anchor.Author.When; - } - - var filter = new CommitFilter - { - IncludeReachableFrom = branchRef.Tip, - SortBy = CommitSortStrategies.Time, - }; - - // Stream commits newest-first; store only SHA strings to avoid holding native - // libgit2 handles beyond the Repository lifetime. Both the --max and no-max - // paths work backward from HEAD and then reverse, which avoids a full - // materialization of the 300K+ commit walk. - var selected = new List(); - DateTimeOffset? lastSelected = null; - - foreach (var commit in repo.Commits.QueryBy(filter)) // newest-first, lazy - { - var commitTime = commit.Author.When; - - // Stop as soon as we pass the resume anchor - if (afterTime.HasValue && commitTime <= afterTime.Value) - break; - - if (lastSelected == null || (lastSelected.Value - commitTime).TotalDays >= intervalDays) - { - selected.Add(new CommitCheckpoint(commit.Sha, commitTime.DateTime)); - lastSelected = commitTime; - - if (maxCheckpoints > 0 && selected.Count >= maxCheckpoints) - break; - } - } - - // Return in chronological order (oldest first) - selected.Reverse(); - return selected; - } - - /// - /// Checks out the target commit in the repository, then walks the filesystem to collect - /// manifest directories and add them to the index. The repository is left at the target - /// commit on return (no state is restored). - /// Retries failures until no further progress can be made (resolves dependency ordering). - /// Returns the count of manifests successfully added. - /// - static int AddAllManifests(IWinGetSQLiteIndex index, string repoPath, string commitSha) - { - Console.WriteLine($" Checking out commit {commitSha[..8]}..."); - RunGit(repoPath, $"checkout --detach {commitSha}"); - - string manifestsRoot = Path.Combine(repoPath, "manifests"); - if (!Directory.Exists(manifestsRoot)) return 0; - - // Collect manifest version directories: any directory that contains .yaml files directly. - Console.WriteLine(" Collecting manifest directories from filesystem..."); - var manifests = Directory - .EnumerateFiles(manifestsRoot, "*.yaml", SearchOption.AllDirectories) - .GroupBy(f => Path.GetDirectoryName(f)!, StringComparer.OrdinalIgnoreCase) - .Select(g => ( - LocalDir: g.Key, - RelPath: Path.GetRelativePath(repoPath, g.Key).Replace(Path.DirectorySeparatorChar, '/'))) - .ToList(); - Console.WriteLine($" Found {manifests.Count} manifest directories"); - - // Initial add pass — collect failures, printing periodic progress. - var failed = new List<(string LocalDir, string RelPath)>(); - int total = manifests.Count; - int done = 0; - foreach (var (localDir, relPath) in manifests) - { - try { index.AddManifest(localDir, relPath); } - catch { failed.Add((localDir, relPath)); } - - done++; - if (done % 500 == 0 || done == total) - Console.Write($"\r Adding: {done}/{total} ({100.0 * done / total:F1}%) "); - } - Console.WriteLine(); // end the \r line - - // Retry loop: keep going as long as at least one failure is resolved each round. - int pass = 1; - while (failed.Count > 0) - { - var retrying = failed; - failed = []; - Console.WriteLine($" Retry pass {pass}: {retrying.Count} manifest(s) pending..."); - foreach (var (localDir, relPath) in retrying) - { - try { index.AddManifest(localDir, relPath); } - catch { failed.Add((localDir, relPath)); } - } - - // No progress this round — stop. - if (failed.Count == retrying.Count) break; - pass++; - } - - foreach (var (_, relPath) in failed) - { - Console.Error.WriteLine($" Could not add manifest (no progress): {relPath}"); - } - - return manifests.Count - failed.Count; - } - - static void RunGit(string repoPath, string arguments) - { - var psi = new ProcessStartInfo("git") - { - Arguments = $"-C \"{repoPath}\" {arguments}", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - }; - using var p = Process.Start(psi)!; - p.WaitForExit(); - if (p.ExitCode != 0) - { - string err = p.StandardError.ReadToEnd().Trim(); - throw new InvalidOperationException($"git {arguments} failed (exit {p.ExitCode}): {err}"); - } - } - - static IEnumerable RunGitLines(string repoPath, string arguments) - { - var psi = new ProcessStartInfo("git") - { - Arguments = $"-C \"{repoPath}\" {arguments}", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - }; - using var p = Process.Start(psi)!; - string? line; - while ((line = p.StandardOutput.ReadLine()) != null) - { - if (!string.IsNullOrEmpty(line)) - yield return line; - } - p.WaitForExit(); - if (p.ExitCode != 0) - { - string err = p.StandardError.ReadToEnd().Trim(); - throw new InvalidOperationException($"git {arguments} failed (exit {p.ExitCode}): {err}"); - } - } - - /// - /// Walks every commit between (exclusive) and - /// (inclusive) in topological order (oldest first) using - /// git rev-list --topo-order --reverse, then applies each commit's manifest - /// change to the index. Each commit is expected to touch at most one manifest directory. - /// Returns the number of index operations that succeeded. - /// - static int ApplyGitDiff(IWinGetSQLiteIndex index, string repoPath, string fromSha, string toSha, string workDir) - { - string tempDir = Path.Combine(workDir, "manifests_diff"); - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); - Directory.CreateDirectory(tempDir); - - // Use git rev-list to obtain the definitive topological ordering (oldest-first). - // This matches exactly what `git log --topo-order --reverse` produces and avoids - // any ambiguity in LibGit2Sharp's CommitSortStrategies.Topological | Reverse. - var commitShas = RunGitLines(repoPath, $"rev-list --topo-order --reverse {fromSha}..{toSha}") - .ToList(); - - if (commitShas.Count == 0) return 0; - - int count = 0; - - string logPath = Path.Combine(workDir, "manifest_operations.txt"); - using var log = new StreamWriter(logPath, append: false, Encoding.UTF8); - log.WriteLine("Commit\tTimestamp\tOperation\tPath\tError"); - - using var repo = new Repository(repoPath); - - foreach (var sha in commitShas) - { - var commit = repo.Lookup(sha); - var parent = commit.Parents.FirstOrDefault(); - var diff = repo.Diff.Compare(parent?.Tree, commit.Tree); - - // Collect per-directory changes within this commit. - // A "move" commit deletes one version dir and adds another — both must be processed. - // Key: directory path. Value: (anyAdded, anyDeleted, anyModified). - var dirChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var change in diff) - { - // For renames, the old and new paths can be in different directories (e.g., a - // version bump moves manifests from /1.0.0/ to /2.0.0/). We must record the - // deletion against the OLD directory and the addition against the NEW directory - // independently; collapsing both sides to a single path would cause the update - // path to look up the wrong tree when extracting the pre-commit state. - - // Old side: record deletion in the source directory. - if (change.Status == ChangeKind.Deleted || change.Status == ChangeKind.Renamed) - { - string oldPath = change.OldPath; - if (oldPath.StartsWith("manifests/", StringComparison.OrdinalIgnoreCase) && - oldPath.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) - { - int lastSlash = oldPath.LastIndexOf('/'); - string dir = lastSlash > 0 ? oldPath[..lastSlash] : string.Empty; - dirChanges.TryGetValue(dir, out var flags); - dirChanges[dir] = (flags.AnyAdded, true, flags.AnyModified); - } - } - - // New side: record addition/modification in the destination directory. - if (change.Status != ChangeKind.Deleted) - { - string newPath = change.Path; - if (newPath.StartsWith("manifests/", StringComparison.OrdinalIgnoreCase) && - newPath.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) - { - int lastSlash = newPath.LastIndexOf('/'); - string dir = lastSlash > 0 ? newPath[..lastSlash] : string.Empty; - dirChanges.TryGetValue(dir, out var flags); - dirChanges[dir] = (change.Status == ChangeKind.Added || change.Status == ChangeKind.Renamed) - ? (true, flags.AnyDeleted, flags.AnyModified) - : (flags.AnyAdded, flags.AnyDeleted, true); - } - } - } - - if (dirChanges.Count == 0) continue; // No manifest changes in this commit - - // Pure deletes first, then updates (remove+add), then pure adds. - // This ensures move commits (delete old dir, add new dir) remove before adding. - static int OpOrder((bool AnyAdded, bool AnyDeleted, bool AnyModified) f) => - (!f.AnyAdded && !f.AnyModified) ? 0 : // pure delete - (!f.AnyDeleted && !f.AnyModified) ? 2 : // pure add - 1; // update (remove+add) - - foreach (var (dirPath, (anyAdded, anyDeleted, anyModified)) in dirChanges.OrderBy(kv => OpOrder(kv.Value))) - { - bool isPureDelete = !anyAdded && !anyModified; - bool isPureAdd = !anyDeleted && !anyModified; - // Everything else is an update: remove the old state then add the new state. - - if (isPureDelete) - { - string localDir = ExtractManifestDirFromTree(repo, parent!, dirPath, tempDir); - TryIndexOp(index, log, commit, "remove", dirPath, localDir, - (idx, dir, path) => idx.RemoveManifest(dir, path), ref count); - } - else if (isPureAdd) - { - // If the directory already existed in the parent tree, this commit is - // adding new files to an existing manifest (e.g., a new locale yaml). - // The package is already in the index, so treat it as an update. - bool dirExistedInParent = parent != null && - parent[dirPath]?.TargetType == TreeEntryTargetType.Tree; - - if (dirExistedInParent) - { - string removeDir = ExtractManifestDirFromTree(repo, parent!, dirPath, tempDir); - TryIndexOp(index, log, commit, "remove", dirPath, removeDir, - (idx, dir, path) => idx.RemoveManifest(dir, path), ref count); - - string addDir = ExtractManifestDirFromTree(repo, commit, dirPath, tempDir); - TryIndexOp(index, log, commit, "add", dirPath, addDir, - (idx, dir, path) => idx.AddManifest(dir, path), ref count); - } - else - { - string localDir = ExtractManifestDirFromTree(repo, commit, dirPath, tempDir); - TryIndexOp(index, log, commit, "add", dirPath, localDir, - (idx, dir, path) => idx.AddManifest(dir, path), ref count); - } - } - else - { - // Update: remove using pre-commit state, then add using post-commit state. - string removeDir = ExtractManifestDirFromTree(repo, parent!, dirPath, tempDir); - TryIndexOp(index, log, commit, "remove", dirPath, removeDir, - (idx, dir, path) => idx.RemoveManifest(dir, path), ref count); - - string addDir = ExtractManifestDirFromTree(repo, commit, dirPath, tempDir); - TryIndexOp(index, log, commit, "add", dirPath, addDir, - (idx, dir, path) => idx.AddManifest(dir, path), ref count); - } - } - } - - Directory.Delete(tempDir, true); - - return count; - } - - static void TryIndexOp(IWinGetSQLiteIndex index, StreamWriter log, Commit commit, - string operationName, string dirPath, string localDir, - Action op, ref int count) - { - try - { - op(index, localDir, dirPath); - log.WriteLine($"{commit.Sha}\t{commit.Author.When:yyyy-MM-dd HH:mm:ss zzz}\t{operationName}\t{dirPath}\t"); - count++; - } - catch (Exception ex) - { - int result = ex.InnerException?.HResult ?? ex.HResult; - log.WriteLine($"{commit.Sha}\t{commit.Author.When:yyyy-MM-dd HH:mm:ss zzz}\t{operationName}\t{dirPath}\t{result}"); - Console.Error.WriteLine($" Failed to {operationName} manifest '{dirPath}': {result}"); - } - } - - /// - /// Extracts all YAML files from the given in the commit's tree - /// into a subdirectory of that includes the first 8 characters - /// of the commit SHA, ensuring no cross-commit directory conflicts. - /// Returns the local directory path used. - /// - static string ExtractManifestDirFromTree(Repository repo, Commit commit, string dirPath, string tempDir) - { - string localDir = Path.Combine( - tempDir, - dirPath.Replace('/', Path.DirectorySeparatorChar), - commit.Sha[..8]); - - var entry = commit[dirPath]; - if (entry?.TargetType != TreeEntryTargetType.Tree) return localDir; - - Directory.CreateDirectory(localDir); - foreach (var child in (Tree)entry.Target) - { - if (child.TargetType == TreeEntryTargetType.Blob && - child.Name.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)) - { - var blob = (Blob)child.Target; - File.WriteAllBytes(Path.Combine(localDir, child.Name), blob.GetContentStream().ReadAllBytes()); - } - } - - return localDir; - } - - /// - /// Compresses using Deflate (the same algorithm used by MSIX/ZIP - /// packaging) and returns the compressed byte count. A temporary file is used so that - /// large files are not loaded into memory. The temporary file is always deleted on return. - /// - static long GetCompressedSize(string filePath) - { - string tempPath = filePath + ".compressed_measure.zip"; - try - { - using (var zipStream = File.Create(tempPath)) - using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, leaveOpen: false)) - { - var entry = archive.CreateEntry(Path.GetFileName(filePath), CompressionLevel.Optimal); - using var entryStream = entry.Open(); - using var sourceStream = File.OpenRead(filePath); - sourceStream.CopyTo(entryStream); - } - return new FileInfo(tempPath).Length; - } - finally - { - if (File.Exists(tempPath)) File.Delete(tempPath); - } - } - - static void WriteCsv(List results, string path) - { - using var writer = new StreamWriter(path, false, Encoding.UTF8); - writer.WriteLine("Index,Date,CommitSha,FullIndexMB,DeltaPrevMB,DeltaOrigMB,FullIndexCompressedMB,DeltaPrevCompressedMB,DeltaOrigCompressedMB"); - - foreach (var r in results) - { - double fullMb = r.FullIndexBytes / 1024.0 / 1024.0; - double deltaPrevMb = r.DeltaPrevBytes / 1024.0 / 1024.0; - double deltaOrigMb = r.DeltaOrigBytes / 1024.0 / 1024.0; - double fullCMb = r.FullIndexCompressedBytes / 1024.0 / 1024.0; - double deltaPrevCMb = r.DeltaPrevCompressedBytes / 1024.0 / 1024.0; - double deltaOrigCMb = r.DeltaOrigCompressedBytes / 1024.0 / 1024.0; - - writer.WriteLine($"{r.Index},{r.Date:yyyy-MM-dd},{r.CommitSha},{fullMb:F2},{deltaPrevMb:F2},{deltaOrigMb:F2},{fullCMb:F2},{deltaPrevCMb:F2},{deltaOrigCMb:F2}"); - } - } - } - - record CommitCheckpoint(string Sha, DateTime Date); - - /// - /// Persisted run parameters and progress, written to state.json in the output directory. - /// - class ToolState - { - [JsonIgnore] - public string StateFilePath { get; set; } = string.Empty; - - public string RepoPath { get; set; } = string.Empty; - public string Branch { get; set; } = string.Empty; - public int IntervalDays { get; set; } = 7; - public List Checkpoints { get; set; } = new(); - - /// - /// Index of the last fully-completed checkpoint, or -1 if none have completed. - /// Updated after each checkpoint succeeds; used by --resume to find the restart point. - /// - public int LastCompleteIndex { get; set; } = -1; - } - - class CheckpointRecord - { - public string Sha { get; set; } = string.Empty; - public DateTime Date { get; set; } - public long FullIndexCompressedBytes { get; set; } - public long DeltaPrevCompressedBytes { get; set; } - public long DeltaOrigCompressedBytes { get; set; } - } - - class CheckpointResult - { - public int Index { get; set; } - public DateTime Date { get; set; } - public string CommitSha { get; set; } = string.Empty; - public long FullIndexBytes { get; set; } - public long DeltaPrevBytes { get; set; } - public long DeltaOrigBytes { get; set; } - public long FullIndexCompressedBytes { get; set; } - public long DeltaPrevCompressedBytes { get; set; } - public long DeltaOrigCompressedBytes { get; set; } - public string? FullIndexPath { get; set; } - public string? PreviousFullIndexPath { get; set; } - } - - static class StreamExtensions - { - public static byte[] ReadAllBytes(this Stream stream) - { - using var ms = new MemoryStream(); - stream.CopyTo(ms); - return ms.ToArray(); - } - } -} diff --git a/tools/DeltaIndexTestTool/analyze.py b/tools/DeltaIndexTestTool/analyze.py deleted file mode 100644 index d39ce06b1c..0000000000 --- a/tools/DeltaIndexTestTool/analyze.py +++ /dev/null @@ -1,491 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -""" -analyze.py - Baseline refresh timing optimizer for winget delta indexes. - -Reads a results.csv produced by DeltaIndexTestTool and models the relative compressed -egress across multiple user updates under different baseline refresh schedules. -Sweeps all candidate refresh periods and recommends the one that minimizes the -expected compressed egress given an assumed user staleness distribution. - -All output is relative (percentages); absolute egress volume is never required. - -COST MODEL SUMMARY ------------------- -The distribution W[D] represents the fraction of *download events* from clients -that were D days stale at the time of download (not the fraction of users). -Telemetry naturally produces this view since it counts downloads, not users. -Frequency is therefore already embedded in W[D] — no additional weighting by 1/D. - -For a refresh period of P checkpoints (P * interval_days days): - - cycle_avg_delta = average of DeltaOrig[0..P-1] - (expected delta size at a random moment in the cycle) - weighted_p_baseline = sum over D of W[D] * min(D, period_days) / period_days - (expected fraction of downloads that need a new baseline) - For "new_client" buckets: p_needs_baseline = 1.0 always. - cost_per_download = cycle_avg_delta + weighted_p_baseline * baseline_size - -The status quo (no deltas at all) costs `baseline_size` per download, so the -predicted traffic reduction for a schedule is simply: - - reduction = 1 - cost_per_download / baseline_size - -BASELINE SIZE -------------- -`baseline_size` is the compressed size of the index a client downloads when it -must take a fresh baseline. By default it is taken from the *last* checkpoint in -the CSV (the most current measurement). Because the tool-built index may not match -what production actually serves, supply the real value with --baseline-mb. - -Delta sizes are NOT scaled along with --baseline-mb: delta growth is driven by the -repository change rate, which holds relatively steady and is measured directly by -the tool. Only the baseline-download side of the model responds to --baseline-mb. - -Key approximation: DeltaOrig growth from baseline 0 is used as a proxy for delta -growth from any hypothetical baseline (reasonable when repository growth is steady). - -DISTRIBUTION FORMAT -------------------- -Buckets can be either: - { "days": N, "weight": W } -- clients N days stale at update time - { "new_client": true, "weight": W } -- net-new clients (no prior index; always - pay full baseline cost) - -Built-in presets: daily_heavy, weekly, monthly - -Telemetry-derived JSON example: - { - "description": "Telemetry-derived YYYY-MM-DD", - "buckets": [ - { "days": 1, "weight": 0.30 }, - { "days": 7, "weight": 0.50 }, - { "days": 30, "weight": 0.15 }, - { "new_client": true, "weight": 0.05 } - ] - } - -Usage: - python analyze.py --csv results.csv --distribution weekly - python analyze.py --csv results.csv --distribution distribution.json - python analyze.py --csv results.csv --distribution weekly --baseline-mb 12.4 --output-chart chart.png -""" - -import argparse -import csv -import json -import sys -import textwrap -from datetime import datetime -from pathlib import Path - -# --------------------------------------------------------------------------- -# Built-in staleness distribution presets -# --------------------------------------------------------------------------- -# Each preset is a PMF over "days since last update" at the moment a user -# triggers an update. Weights must sum to 1.0. -# Replace buckets with telemetry-derived data when available — no other code -# changes are needed; supply a JSON file matching this format via --distribution. - -PRESETS = { - "daily_heavy": { - "description": "Assumption (download events): heavy automated/CI usage — many daily downloads", - "buckets": [ - {"days": 1, "weight": 0.80}, - {"days": 7, "weight": 0.15}, - {"days": 30, "weight": 0.04}, - {"days": 90, "weight": 0.01}, - ], - }, - "weekly": { - "description": "Assumption (download events): typical developer tool — weekly updaters dominate downloads", - "buckets": [ - {"days": 1, "weight": 0.30}, - {"days": 7, "weight": 0.50}, - {"days": 30, "weight": 0.15}, - {"days": 90, "weight": 0.05}, - ], - }, - "monthly": { - "description": "Assumption (download events): infrequent updaters dominate downloads", - "buckets": [ - {"days": 1, "weight": 0.05}, - {"days": 7, "weight": 0.25}, - {"days": 30, "weight": 0.45}, - {"days": 90, "weight": 0.25}, - ], - }, -} - - -# --------------------------------------------------------------------------- -# Data loading -# --------------------------------------------------------------------------- - -def load_distribution(dist_arg): - """Return a distribution dict from a preset name or a JSON file path.""" - if dist_arg in PRESETS: - return PRESETS[dist_arg] - p = Path(dist_arg) - if not p.exists(): - raise FileNotFoundError(f"Distribution file not found: {dist_arg}") - with p.open(encoding="utf-8") as f: - return json.load(f) - - -def load_csv(csv_path): - """ - Load checkpoints from results.csv. Returns a list of dicts (one per row). - If compressed-size columns are absent (produced before that feature was added), - falls back to the uncompressed values so older CSVs remain usable. - """ - rows = [] - with open(csv_path, newline="", encoding="utf-8") as f: - reader = csv.DictReader(f) - for row in reader: - r = {} - r["Date"] = datetime.strptime(row["Date"].strip(), "%Y-%m-%d") - r["CommitSha"] = row.get("CommitSha", "").strip() - for col in ("FullIndexMB", "DeltaPrevMB", "DeltaOrigMB"): - r[col] = float(row.get(col) or 0) - # Compressed columns — fall back gracefully to uncompressed values. - r["FullIndexCompressedMB"] = float( - row.get("FullIndexCompressedMB") or r["FullIndexMB"]) - r["DeltaPrevCompressedMB"] = float( - row.get("DeltaPrevCompressedMB") or r["DeltaPrevMB"]) - r["DeltaOrigCompressedMB"] = float( - row.get("DeltaOrigCompressedMB") or r["DeltaOrigMB"]) - rows.append(r) - return rows - - -# --------------------------------------------------------------------------- -# Cost model -# --------------------------------------------------------------------------- - -def compute_interval_days(checkpoints): - """Estimate the average days between consecutive checkpoints.""" - if len(checkpoints) < 2: - return 7 - span = (checkpoints[-1]["Date"] - checkpoints[0]["Date"]).days - return span / (len(checkpoints) - 1) - - -def normalize_buckets(buckets): - """Return a copy of buckets with weights normalized to sum to 1.0. - - Supports both regular staleness buckets {"days": N, "weight": W} and - net-new client buckets {"new_client": true, "weight": W}. - """ - total = sum(b["weight"] for b in buckets) - if abs(total - 1.0) > 0.01: - print(f"Warning: distribution weights sum to {total:.3f}, normalizing to 1.0", - file=sys.stderr) - result = [] - for b in buckets: - normalized = {"weight": b["weight"] / total} - if b.get("new_client"): - normalized["new_client"] = True - else: - normalized["days"] = b["days"] - result.append(normalized) - return result - - -def simulate_schedule(checkpoints, period, buckets, interval_days, baseline_size): - """ - Simulate a periodic baseline refresh every `period` checkpoints and return - the expected compressed egress cost per download event. - - The distribution buckets represent fractions of *download events* by client - staleness (D days since last update). Since frequency is already embedded - in the weights, no additional per-user-type frequency scaling is applied. - - For a given period P: - - cycle_avg_delta = mean(DeltaOrig[0], ..., DeltaOrig[P-1]) - Expected delta size at a uniformly random moment in - the baseline lifecycle. - - weighted_p_baseline = sum over D of: W[D] * min(D, period_days) / period_days - Expected fraction of download events where the client's - index predates the current baseline, requiring a full - baseline download. - - cost_per_download = cycle_avg_delta + weighted_p_baseline * baseline_size - - `baseline_size` is the compressed MB a client pays for a fresh baseline; see - the module docstring for how it is chosen. Delta sizes come from measured - repository change rate and are deliberately independent of it. - - Returns cost_per_download (MB). Only ratios between schedules (and against - `baseline_size`, the status quo) are meaningful. - """ - delta_curve = [cp["DeltaOrigCompressedMB"] for cp in checkpoints] - period_days = period * interval_days - - cycle_deltas = [delta_curve[min(a, len(delta_curve) - 1)] for a in range(period)] - cycle_avg_delta = sum(cycle_deltas) / period - - weighted_p_baseline = sum( - b["weight"] * (1.0 if b.get("new_client") else min(b["days"], period_days) / period_days) - for b in buckets - ) - - return cycle_avg_delta + weighted_p_baseline * baseline_size - - -def find_crossover(checkpoints, threshold, baseline_size): - """ - Return the index of the first checkpoint where - DeltaOrigCompressedMB / baseline_size >= threshold, or None. - """ - if baseline_size <= 0: - return None - for i, cp in enumerate(checkpoints): - if cp["DeltaOrigCompressedMB"] / baseline_size >= threshold: - return i - return None - - -# --------------------------------------------------------------------------- -# Formatting helpers -# --------------------------------------------------------------------------- - -def fmt_mb(mb): - """Format a MB value as MB or GB depending on magnitude.""" - if mb >= 1024: - return f"{mb / 1024:.2f} GB" - return f"{mb:.1f} MB" - - -def fmt_days(days): - """Format a number of days as 'd' or 'wk' shorthand.""" - if days % 7 == 0 and days >= 7: - return f"~{int(days // 7)}wk" - return f"~{days:.0f}d" - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - -def main(): - # Windows consoles often default to cp1252, which cannot encode the arrows and - # dashes used below. Prefer UTF-8, and degrade to replacement chars if unavailable. - for stream in (sys.stdout, sys.stderr): - try: - stream.reconfigure(encoding="utf-8", errors="replace") - except (AttributeError, ValueError): - pass - - parser = argparse.ArgumentParser( - description="Optimize baseline refresh timing for winget delta indexes.", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog="Built-in distribution presets: " + ", ".join(PRESETS), - ) - parser.add_argument("--csv", required=True, - help="Path to results.csv from DeltaIndexTestTool") - parser.add_argument("--distribution", required=True, - help="Staleness distribution: preset name or path to JSON file. " - f"Presets: {', '.join(PRESETS)}") - parser.add_argument("--baseline-mb", type=float, default=None, dest="baseline_mb", - help="Compressed size (MB) of the baseline index clients actually download. " - "Defaults to the last checkpoint's FullIndexCompressedMB from the CSV. " - "Delta sizes are not scaled by this value.") - parser.add_argument("--output-chart", default=None, dest="output_chart", - help="Optional path to save a chart image (e.g. chart.png). Requires matplotlib.") - args = parser.parse_args() - - # --- Load inputs -------------------------------------------------------- - checkpoints = load_csv(args.csv) - if not checkpoints: - print("Error: no checkpoints in CSV.", file=sys.stderr) - sys.exit(1) - - dist = load_distribution(args.distribution) - buckets = normalize_buckets(dist["buckets"]) - description = dist.get("description", args.distribution) - - interval_days = compute_interval_days(checkpoints) - n = len(checkpoints) - - # The size a client pays for a fresh baseline. The CSV's last checkpoint is - # the most current measurement, but production may serve a different index; - # --baseline-mb lets telemetry-observed reality drive the model instead. - csv_baseline_size = checkpoints[-1]["FullIndexCompressedMB"] - baseline_size = args.baseline_mb if args.baseline_mb is not None else csv_baseline_size - if baseline_size <= 0: - print("Error: baseline size is zero; supply --baseline-mb.", file=sys.stderr) - sys.exit(1) - - # --- Header ------------------------------------------------------------- - print(f"\n{'='*68}") - print(f" Baseline Timing Analysis") - print(f"{'='*68}") - print(f" CSV: {args.csv}") - print(f" Checkpoints: {n} " - f"({checkpoints[0]['Date']:%Y-%m-%d} to {checkpoints[-1]['Date']:%Y-%m-%d})") - print(f" Avg interval: {interval_days:.1f} days " - f"(total span: {(checkpoints[-1]['Date'] - checkpoints[0]['Date']).days} days)") - print(f" Distribution: {description}") - if args.baseline_mb is not None: - print(f" Baseline: {fmt_mb(baseline_size)} (supplied; " - f"CSV measured {fmt_mb(csv_baseline_size)})") - else: - print(f" Baseline: {fmt_mb(baseline_size)} (last checkpoint in CSV)") - print() - - # --- Delta growth crossovers ------------------------------------------- - c50 = find_crossover(checkpoints, 0.50, baseline_size) - c100 = find_crossover(checkpoints, 1.00, baseline_size) - print(" Delta (compressed) growth from baseline:") - if c50 is not None: - print(f" Exceeds 50% of baseline at checkpoint {c50:>3} " - f"({fmt_days(c50 * interval_days)})") - else: - print(" Never exceeds 50% of baseline within measured period") - if c100 is not None: - print(f" Exceeds 100% of baseline at checkpoint {c100:>3} " - f"({fmt_days(c100 * interval_days)})") - else: - print(" Never exceeds 100% of baseline within measured period") - print() - - # --- Simulate all periods ----------------------------------------------- - results = [] - for period in range(1, n + 1): - cost_per_dl = simulate_schedule(checkpoints, period, buckets, interval_days, baseline_size) - results.append({ - "period": period, - "period_days": period * interval_days, - "total_mb": cost_per_dl, # MB per download event - }) - - optimal = min(results, key=lambda r: r["total_mb"]) - never_refresh = results[-1] # period == n - - # Status quo: no deltas at all, every download fetches the whole index. - status_quo_mb = baseline_size - - # --- Determine which periods to print in the table ---------------------- - # Always show: period 1, optimal, and period n. - # Also show a sample of ~15-20 evenly-spaced periods in between. - show = {1, optimal["period"], n} - step = max(1, n // 18) - for p in range(step, n, step): - show.add(p) - - # --- Print table -------------------------------------------------------- - print(f"--- Schedule Comparison (expected compressed egress per download event) ---") - print(f" (lower = cheaper per update on average; % is reduction vs. today's " - f"full-index-every-time behavior)") - print(f" {'Period':>6} {'Interval':>9} {'MB/Download':>13} {'vs Status Quo':>14}") - print(f" {'-'*6} {'-'*9} {'-'*13} {'-'*14}") - - print(f" {'-':>6} {'-':>9} {status_quo_mb:>11.2f} MB " - f"{'baseline':>14} (status quo: full index every download)") - - for r in sorted(results, key=lambda r: r["period"]): - if r["period"] not in show: - continue - tag = "" - if r["period"] == n: - tag = " (never refresh)" - elif r["period"] == optimal["period"]: - tag = " ← optimal" - - reduction = 100.0 * (1.0 - r["total_mb"] / status_quo_mb) - print(f" {r['period']:>6} {fmt_days(r['period_days']):>9} " - f"{r['total_mb']:>11.2f} MB {reduction:>13.1f}%{tag}") - - print() - - # --- Recommendation summary --------------------------------------------- - reduction_vs_status_quo = 100.0 * (1.0 - optimal["total_mb"] / status_quo_mb) - savings_vs_never = (100.0 * (1.0 - optimal["total_mb"] / never_refresh["total_mb"]) - if never_refresh["total_mb"] > 0 else 0.0) - - print(f" Recommendation: refresh baseline every {optimal['period']} checkpoint(s) " - f"({fmt_days(optimal['period_days'])})") - print(f" {optimal['total_mb']:.2f} MB / download event") - print(f" Predicted outbound traffic reduction: {reduction_vs_status_quo:.1f}%") - if optimal["period"] < n: - print(f" vs never-refresh: {savings_vs_never:+.1f}%") - print() - - # --- Chart -------------------------------------------------------------- - if args.output_chart: - _write_chart(args.output_chart, checkpoints, optimal, interval_days, - description, baseline_size) - - -def _mb_precision(ticks): - """Decimal places needed so adjacent axis ticks render as distinct labels.""" - spacing = min((abs(b - a) for a, b in zip(ticks, ticks[1:])), default=1.0) - if spacing >= 1.0: - return 0 - if spacing >= 0.1: - return 1 - return 2 - - -def _write_chart(path, checkpoints, optimal, interval_days, description, baseline_size): - try: - import matplotlib - matplotlib.use("Agg") - import matplotlib.pyplot as plt - import matplotlib.ticker as ticker - except ImportError: - print(" Note: matplotlib not available; skipping chart. " - "Install with: pip install matplotlib", file=sys.stderr) - return - - dates = [cp["Date"] for cp in checkpoints] - full_vals = [cp["FullIndexCompressedMB"] for cp in checkpoints] - delta_vals = [cp["DeltaOrigCompressedMB"] for cp in checkpoints] - n = len(checkpoints) - - fig, ax = plt.subplots(figsize=(13, 6)) - - ax.plot(dates, full_vals, label="Full index (compressed)", color="#C0392B", linewidth=2) - ax.plot(dates, delta_vals, label="Delta from baseline (compressed)", color="#27AE60", linewidth=2) - ax.axhline(y=baseline_size, color="#7F8C8D", linestyle=":", linewidth=1.5, - label=f"Baseline download size ({baseline_size:.1f} MB)") - - # Vertical lines at recommended baseline refresh points - opt_period = optimal["period"] - baseline_indices = list(range(opt_period, n, opt_period)) - first_line = True - for bi in baseline_indices: - lbl = (f"Recommended baseline ({fmt_days(opt_period * interval_days)} period)" - if first_line else None) - ax.axvline(x=checkpoints[bi]["Date"], color="#2980B9", - linestyle="--", linewidth=0.9, alpha=0.7, label=lbl) - first_line = False - - # Shade the area between the curves for visual clarity - ax.fill_between(dates, delta_vals, full_vals, - where=[d < f for d, f in zip(delta_vals, full_vals)], - alpha=0.07, color="#27AE60", label="Potential savings region") - - ax.set_xlabel("Date") - ax.set_ylabel("Compressed Size (MB)") - ax.set_title("Delta Index Baseline Timing Analysis\n" - + "\n".join(textwrap.wrap(f"Distribution: {description}", 110)), - fontsize=11) - ax.legend(loc="upper left") - # Whole-MB labels collapse into duplicates when the plotted range is only a few MB, - # so pick the precision from the actual tick spacing. - ax.yaxis.set_major_formatter(ticker.FuncFormatter( - lambda v, _: f"{v:.{_mb_precision(ax.get_yticks())}f} MB")) - fig.autofmt_xdate() - plt.tight_layout() - plt.savefig(path, dpi=150) - plt.close(fig) - print(f" Chart saved to: {path}") - - -if __name__ == "__main__": - main() diff --git a/tools/DeltaIndexTestTool/baseline-analysis.png b/tools/DeltaIndexTestTool/baseline-analysis.png deleted file mode 100644 index 262156c7a9fdddcd1ad3f258769f2e010a40ffc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 136103 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 diff --git a/tools/DeltaIndexTestTool/dist-observed.json b/tools/DeltaIndexTestTool/dist-observed.json deleted file mode 100644 index 8cb9f08900..0000000000 --- a/tools/DeltaIndexTestTool/dist-observed.json +++ /dev/null @@ -1,7205 +0,0 @@ -{ - "description": "Telemetry-derived from C:\\Users\\johnmcp\\Downloads\\export.csv: 549,093 download events, 2.21% net-new clients, observed ages 0-18.0 days", - "buckets": [ - { - "days": 0.0, - "weight": 5.099318330410331e-05 - }, - { - "days": 0.000694, - "weight": 0.0005408919800470958 - }, - { - "days": 0.028472, - "weight": 0.002178137401132413 - }, - { - "days": 0.042361, - "weight": 5.827792377611807e-05 - }, - { - "days": 0.043056, - "weight": 8.195333031016604e-05 - }, - { - "days": 0.047222, - "weight": 0.008171657624482557 - }, - { - "days": 0.049306, - "weight": 5.099318330410331e-05 - }, - { - "days": 0.050694, - "weight": 0.0035476686098711878 - }, - { - "days": 0.051389, - "weight": 0.00886006559908795 - }, - { - "days": 0.052083, - "weight": 3.6423702360073794e-05 - }, - { - "days": 0.054167, - "weight": 0.005119351366708371 - }, - { - "days": 0.054861, - "weight": 0.008016856889452242 - }, - { - "days": 0.055556, - "weight": 0.006589047756937349 - }, - { - "days": 0.056944, - "weight": 0.007004277963842191 - }, - { - "days": 0.057639, - "weight": 0.009665029421245582 - }, - { - "days": 0.058333, - "weight": 0.00428889095289869 - }, - { - "days": 0.059028, - "weight": 0.04424933481213565 - }, - { - "days": 0.059722, - "weight": 0.02019694295866092 - }, - { - "days": 0.060417, - "weight": 0.008477616724307175 - }, - { - "days": 0.061111, - "weight": 0.018180891033030836 - }, - { - "days": 0.061806, - "weight": 0.020333531842511195 - }, - { - "days": 0.0625, - "weight": 0.0037935286008016857 - }, - { - "days": 0.063194, - "weight": 0.014030410149100425 - }, - { - "days": 0.063889, - "weight": 0.01562758949758966 - }, - { - "days": 0.064583, - "weight": 0.003698826974665494 - }, - { - "days": 0.065278, - "weight": 0.008226293178022667 - }, - { - "days": 0.065972, - "weight": 0.010284232361366837 - }, - { - "days": 0.066667, - "weight": 0.016946127523024334 - }, - { - "days": 0.067361, - "weight": 0.0046877304937414975 - }, - { - "days": 0.069444, - "weight": 0.006570835905757313 - }, - { - "days": 0.070833, - "weight": 0.004013892000080132 - }, - { - "days": 0.072222, - "weight": 0.004247003695184604 - }, - { - "days": 0.072917, - "weight": 0.003726144751435549 - }, - { - "days": 0.074306, - "weight": 0.012977765150894292 - }, - { - "days": 0.076389, - "weight": 6.556266424813284e-05 - }, - { - "days": 0.078472, - "weight": 0.005257761435676652 - }, - { - "days": 0.084028, - "weight": 0.0034893906860950694 - }, - { - "days": 0.084722, - "weight": 0.005062894628050258 - }, - { - "days": 0.085417, - "weight": 0.005155775069068446 - }, - { - "days": 0.0875, - "weight": 0.00024039643557648705 - }, - { - "days": 0.088889, - "weight": 0.0024258185771809148 - }, - { - "days": 0.102083, - "weight": 0.0013039685444906418 - }, - { - "days": 0.109722, - "weight": 0.0012493329909505312 - }, - { - "days": 0.110417, - "weight": 0.0020124095553940772 - }, - { - "days": 0.113194, - "weight": 0.00502464974057218 - }, - { - "days": 0.114583, - "weight": 0.0016135700145512692 - }, - { - "days": 0.115278, - "weight": 7.284740472014759e-05 - }, - { - "days": 0.115972, - "weight": 4.188725771408486e-05 - }, - { - "days": 0.116667, - "weight": 0.0016354242359673133 - }, - { - "days": 0.117361, - "weight": 0.003261742546344608 - }, - { - "days": 0.118056, - "weight": 0.0033546229873627964 - }, - { - "days": 0.11875, - "weight": 0.002159925549952376 - }, - { - "days": 0.119444, - "weight": 0.0017009869002154463 - }, - { - "days": 0.120139, - "weight": 0.002194528067194446 - }, - { - "days": 0.120833, - "weight": 0.001914065559021878 - }, - { - "days": 0.121528, - "weight": 0.0019413833357919332 - }, - { - "days": 0.122222, - "weight": 0.001766549564463579 - }, - { - "days": 0.122917, - "weight": 0.0014132396515708632 - }, - { - "days": 0.123611, - "weight": 0.0006774808638973725 - }, - { - "days": 0.124306, - "weight": 0.002003303629804059 - }, - { - "days": 0.125, - "weight": 0.000744864713263509 - }, - { - "days": 0.125694, - "weight": 0.004664055087207449 - }, - { - "days": 0.126389, - "weight": 0.0008595993756977415 - }, - { - "days": 0.127083, - "weight": 0.0027973403412536674 - }, - { - "days": 0.127778, - "weight": 0.0028191945626697117 - }, - { - "days": 0.130556, - "weight": 0.00030960147006062726 - }, - { - "days": 0.13125, - "weight": 0.0014660540199929702 - }, - { - "days": 0.131944, - "weight": 0.0007831096007415866 - }, - { - "days": 0.132639, - "weight": 0.0015134048330610662 - }, - { - "days": 0.133333, - "weight": 0.003010419000060099 - }, - { - "days": 0.134028, - "weight": 0.0023384016915167375 - }, - { - "days": 0.134722, - "weight": 0.0007211893067294612 - }, - { - "days": 0.136111, - "weight": 0.0014879082414090145 - }, - { - "days": 0.1375, - "weight": 0.0006210241252392582 - }, - { - "days": 0.138194, - "weight": 0.0005481767205191106 - }, - { - "days": 0.14375, - "weight": 0.00043526324320288185 - }, - { - "days": 0.144444, - "weight": 0.0012839355081926012 - }, - { - "days": 0.147917, - "weight": 0.001180127956466391 - }, - { - "days": 0.148611, - "weight": 0.0002658930272285387 - }, - { - "days": 0.149306, - "weight": 0.0009998306297840257 - }, - { - "days": 0.150694, - "weight": 0.001367710023620771 - }, - { - "days": 0.161111, - "weight": 0.0007721824900335645 - }, - { - "days": 0.164583, - "weight": 3.8244887478077486e-05 - }, - { - "days": 0.168056, - "weight": 0.0007612553793255423 - }, - { - "days": 0.169444, - "weight": 0.0017100928258054647 - }, - { - "days": 0.172222, - "weight": 0.0007612553793255423 - }, - { - "days": 0.172917, - "weight": 6.738384936613652e-05 - }, - { - "days": 0.173611, - "weight": 0.0011309559582802914 - }, - { - "days": 0.175, - "weight": 0.000575494497289166 - }, - { - "days": 0.175694, - "weight": 0.0014004913557448375 - }, - { - "days": 0.176389, - "weight": 0.0006501630871273172 - }, - { - "days": 0.177083, - "weight": 0.0011182076624542655 - }, - { - "days": 0.177778, - "weight": 0.0020615815535801768 - }, - { - "days": 0.178472, - "weight": 0.0006720173085433616 - }, - { - "days": 0.179167, - "weight": 0.002440388058124944 - }, - { - "days": 0.179861, - "weight": 0.0011236712178082765 - }, - { - "days": 0.18125, - "weight": 0.002658930272285387 - }, - { - "days": 0.181944, - "weight": 0.0014460209836949297 - }, - { - "days": 0.182639, - "weight": 0.0005846004228791844 - }, - { - "days": 0.183333, - "weight": 0.0005827792377611807 - }, - { - "days": 0.184028, - "weight": 0.0011054593666282397 - }, - { - "days": 0.184722, - "weight": 0.001502477722353044 - }, - { - "days": 0.185417, - "weight": 0.000768540119797557 - }, - { - "days": 0.186111, - "weight": 0.00275909545377559 - }, - { - "days": 0.186806, - "weight": 0.0003314556914766715 - }, - { - "days": 0.1875, - "weight": 0.000475329315798963 - }, - { - "days": 0.188889, - "weight": 0.0008741688566417711 - }, - { - "days": 0.190972, - "weight": 0.0005354284246930847 - }, - { - "days": 0.192361, - "weight": 0.0015625768312471657 - }, - { - "days": 0.194444, - "weight": 0.0007066198257854316 - }, - { - "days": 0.195139, - "weight": 0.003063233368482206 - }, - { - "days": 0.196528, - "weight": 0.0012639024718945607 - }, - { - "days": 0.197222, - "weight": 0.0004425479836748966 - }, - { - "days": 0.197917, - "weight": 0.000287747248644583 - }, - { - "days": 0.2, - "weight": 0.0003951971706068007 - }, - { - "days": 0.202083, - "weight": 0.0016117488294332654 - }, - { - "days": 0.204167, - "weight": 0.0006756596787793689 - }, - { - "days": 0.208333, - "weight": 0.0007248316769654685 - }, - { - "days": 0.210417, - "weight": 0.0008832747822317895 - }, - { - "days": 0.211111, - "weight": 0.0005408919800470958 - }, - { - "days": 0.2125, - "weight": 0.000657447827599332 - }, - { - "days": 0.218056, - "weight": 0.00022582695463245752 - }, - { - "days": 0.222917, - "weight": 0.00036423702360073796 - }, - { - "days": 0.227083, - "weight": 0.0003496675426567084 - }, - { - "days": 0.229167, - "weight": 0.00011837703267023983 - }, - { - "days": 0.23125, - "weight": 0.0008013214519216235 - }, - { - "days": 0.232639, - "weight": 0.00013294651361426936 - }, - { - "days": 0.233333, - "weight": 0.00010927110708022139 - }, - { - "days": 0.234722, - "weight": 0.00012019821778824352 - }, - { - "days": 0.235417, - "weight": 0.00011655584755223614 - }, - { - "days": 0.236111, - "weight": 0.00030778028494262354 - }, - { - "days": 0.2375, - "weight": 0.0003041379147066162 - }, - { - "days": 0.238194, - "weight": 0.0001256617731422546 - }, - { - "days": 0.238889, - "weight": 0.000475329315798963 - }, - { - "days": 0.239583, - "weight": 0.0002531447314025129 - }, - { - "days": 0.240278, - "weight": 0.00046440220509094086 - }, - { - "days": 0.240972, - "weight": 0.0004935411669789999 - }, - { - "days": 0.242361, - "weight": 0.0004061242813148228 - }, - { - "days": 0.243056, - "weight": 0.0009014866334118264 - }, - { - "days": 0.24375, - "weight": 0.0006210241252392582 - }, - { - "days": 0.244444, - "weight": 0.00012748295826025828 - }, - { - "days": 0.245833, - "weight": 0.0002695353974645461 - }, - { - "days": 0.247917, - "weight": 0.00034056161706669 - }, - { - "days": 0.248611, - "weight": 9.105925590018449e-05 - }, - { - "days": 0.249306, - "weight": 0.00027864132305456454 - }, - { - "days": 0.251389, - "weight": 0.0001420524392042878 - }, - { - "days": 0.252083, - "weight": 0.0013185380254346713 - }, - { - "days": 0.252778, - "weight": 0.00030778028494262354 - }, - { - "days": 0.253472, - "weight": 0.0005208589437490553 - }, - { - "days": 0.254167, - "weight": 0.00026225065699253135 - }, - { - "days": 0.254861, - "weight": 0.0001238405880242509 - }, - { - "days": 0.255556, - "weight": 0.00014933717967630255 - }, - { - "days": 0.25625, - "weight": 0.00013476769873227304 - }, - { - "days": 0.258333, - "weight": 0.00029138961888059035 - }, - { - "days": 0.259028, - "weight": 9.834399637219924e-05 - }, - { - "days": 0.259722, - "weight": 8.74168856641771e-05 - }, - { - "days": 0.261111, - "weight": 0.0002822836932905719 - }, - { - "days": 0.265278, - "weight": 0.00016754903085633945 - }, - { - "days": 0.268056, - "weight": 0.00022218458439645015 - }, - { - "days": 0.271528, - "weight": 0.0004990047223330109 - }, - { - "days": 0.274306, - "weight": 0.00011837703267023983 - }, - { - "days": 0.275, - "weight": 0.00012748295826025828 - }, - { - "days": 0.276389, - "weight": 0.0001438736243222915 - }, - { - "days": 0.279861, - "weight": 0.0002531447314025129 - }, - { - "days": 0.281944, - "weight": 0.00017847614156436158 - }, - { - "days": 0.284028, - "weight": 4.7350813068095936e-05 - }, - { - "days": 0.286806, - "weight": 0.00014569480944029518 - }, - { - "days": 0.288194, - "weight": 9.834399637219924e-05 - }, - { - "days": 0.290278, - "weight": 0.0002658930272285387 - }, - { - "days": 0.292361, - "weight": 0.00012201940290624721 - }, - { - "days": 0.293056, - "weight": 0.000158443105266321 - }, - { - "days": 0.295139, - "weight": 0.00014569480944029518 - }, - { - "days": 0.297222, - "weight": 8.74168856641771e-05 - }, - { - "days": 0.297917, - "weight": 0.0004389056134388892 - }, - { - "days": 0.299306, - "weight": 0.0004953623520970036 - }, - { - "days": 0.3, - "weight": 0.0002695353974645461 - }, - { - "days": 0.300694, - "weight": 0.0003715217640727527 - }, - { - "days": 0.301389, - "weight": 0.00022946932486846491 - }, - { - "days": 0.303472, - "weight": 8.195333031016604e-05 - }, - { - "days": 0.304167, - "weight": 0.0003842700598987785 - }, - { - "days": 0.305556, - "weight": 0.0003951971706068007 - }, - { - "days": 0.30625, - "weight": 0.00022036339927844646 - }, - { - "days": 0.306944, - "weight": 0.0001748337713283542 - }, - { - "days": 0.308333, - "weight": 0.0003023167295886125 - }, - { - "days": 0.309722, - "weight": 0.001273008397484579 - }, - { - "days": 0.310417, - "weight": 0.0001438736243222915 - }, - { - "days": 0.311111, - "weight": 0.00020215154809840956 - }, - { - "days": 0.311806, - "weight": 0.00048443524138898147 - }, - { - "days": 0.3125, - "weight": 0.0003059590998246199 - }, - { - "days": 0.313194, - "weight": 0.00044619035391090396 - }, - { - "days": 0.313889, - "weight": 0.0001384100689682804 - }, - { - "days": 0.314583, - "weight": 0.00018758206715438003 - }, - { - "days": 0.315972, - "weight": 0.0001402312540862841 - }, - { - "days": 0.316667, - "weight": 0.00010927110708022139 - }, - { - "days": 0.317361, - "weight": 0.00013476769873227304 - }, - { - "days": 0.31875, - "weight": 5.2814368422107004e-05 - }, - { - "days": 0.320139, - "weight": 0.00037516413430876007 - }, - { - "days": 0.320833, - "weight": 0.00010016518149020294 - }, - { - "days": 0.322222, - "weight": 0.00020579391833441693 - }, - { - "days": 0.323611, - "weight": 5.4635553540110695e-05 - }, - { - "days": 0.326389, - "weight": 0.00018940325227238374 - }, - { - "days": 0.327778, - "weight": 0.00015297954991230994 - }, - { - "days": 0.33125, - "weight": 0.00022400576951445383 - }, - { - "days": 0.332639, - "weight": 0.007545169943889286 - }, - { - "days": 0.335417, - "weight": 0.00020943628857042433 - }, - { - "days": 0.338194, - "weight": 0.00010562873684421401 - }, - { - "days": 0.340278, - "weight": 0.00017301258621035053 - }, - { - "days": 0.340972, - "weight": 0.0003769853194267638 - }, - { - "days": 0.343056, - "weight": 0.00014569480944029518 - }, - { - "days": 0.34375, - "weight": 0.00018393969691837266 - }, - { - "days": 0.345139, - "weight": 3.46025172420701e-05 - }, - { - "days": 0.345833, - "weight": 0.0001748337713283542 - }, - { - "days": 0.349306, - "weight": 0.00011837703267023983 - }, - { - "days": 0.353472, - "weight": 0.00014933717967630255 - }, - { - "days": 0.354861, - "weight": 0.00013658888385027673 - }, - { - "days": 0.356944, - "weight": 8.559570054617342e-05 - }, - { - "days": 0.357639, - "weight": 0.00017119140109234684 - }, - { - "days": 0.358333, - "weight": 7.466858983815127e-05 - }, - { - "days": 0.359028, - "weight": 0.0006701961234253578 - }, - { - "days": 0.359722, - "weight": 0.00027499895281855717 - }, - { - "days": 0.360417, - "weight": 0.00021854221416044278 - }, - { - "days": 0.361111, - "weight": 0.00012748295826025828 - }, - { - "days": 0.361806, - "weight": 5.4635553540110695e-05 - }, - { - "days": 0.3625, - "weight": 0.0003423828021846937 - }, - { - "days": 0.363194, - "weight": 0.00025678710163852027 - }, - { - "days": 0.363889, - "weight": 0.00010198636660820662 - }, - { - "days": 0.365972, - "weight": 0.00021489984392443538 - }, - { - "days": 0.366667, - "weight": 0.00010380755172621031 - }, - { - "days": 0.367361, - "weight": 7.648977495615497e-05 - }, - { - "days": 0.36875, - "weight": 0.00012748295826025828 - }, - { - "days": 0.369444, - "weight": 0.000158443105266321 - }, - { - "days": 0.370139, - "weight": 0.0003023167295886125 - }, - { - "days": 0.371528, - "weight": 0.0004735081306809593 - }, - { - "days": 0.372222, - "weight": 0.0001802973266823653 - }, - { - "days": 0.372917, - "weight": 0.0002513235462845092 - }, - { - "days": 0.373611, - "weight": 0.00022400576951445383 - }, - { - "days": 0.375, - "weight": 0.0018321122287117118 - }, - { - "days": 0.376389, - "weight": 0.0004316208729668745 - }, - { - "days": 0.377083, - "weight": 0.00017301258621035053 - }, - { - "days": 0.377778, - "weight": 0.00020397273321641325 - }, - { - "days": 0.378472, - "weight": 0.0001802973266823653 - }, - { - "days": 0.379861, - "weight": 0.00014569480944029518 - }, - { - "days": 0.38125, - "weight": 0.00015297954991230994 - }, - { - "days": 0.3875, - "weight": 0.0001438736243222915 - }, - { - "days": 0.388889, - "weight": 5.645673865811438e-05 - }, - { - "days": 0.390278, - "weight": 0.00012930414337826196 - }, - { - "days": 0.392361, - "weight": 0.00012930414337826196 - }, - { - "days": 0.397222, - "weight": 0.00023493288022247597 - }, - { - "days": 0.398611, - "weight": 0.0002658930272285387 - }, - { - "days": 0.399306, - "weight": 0.00014569480944029518 - }, - { - "days": 0.400694, - "weight": 0.00023857525045848336 - }, - { - "days": 0.404167, - "weight": 0.00014751599455829886 - }, - { - "days": 0.406944, - "weight": 0.0020761510345242064 - }, - { - "days": 0.407639, - "weight": 0.00018211851180036898 - }, - { - "days": 0.409028, - "weight": 0.0001402312540862841 - }, - { - "days": 0.4125, - "weight": 0.0002950319891165977 - }, - { - "days": 0.414583, - "weight": 8.013214519216235e-05 - }, - { - "days": 0.415972, - "weight": 9.834399637219924e-05 - }, - { - "days": 0.418056, - "weight": 0.0003041379147066162 - }, - { - "days": 0.41875, - "weight": 0.00012019821778824352 - }, - { - "days": 0.419444, - "weight": 0.00023857525045848336 - }, - { - "days": 0.420139, - "weight": 0.00034602517242070106 - }, - { - "days": 0.421528, - "weight": 0.0004735081306809593 - }, - { - "days": 0.422222, - "weight": 0.00023675406534047965 - }, - { - "days": 0.422917, - "weight": 9.652281125419556e-05 - }, - { - "days": 0.424306, - "weight": 0.00024403880581249442 - }, - { - "days": 0.425, - "weight": 0.00034420398730269734 - }, - { - "days": 0.426389, - "weight": 0.00013658888385027673 - }, - { - "days": 0.427083, - "weight": 0.00023857525045848336 - }, - { - "days": 0.427778, - "weight": 0.0003041379147066162 - }, - { - "days": 0.429861, - "weight": 0.0017592648239915642 - }, - { - "days": 0.43125, - "weight": 5.4635553540110695e-05 - }, - { - "days": 0.431944, - "weight": 0.00019850917786240217 - }, - { - "days": 0.432639, - "weight": 0.00024221762069449073 - }, - { - "days": 0.434028, - "weight": 0.0002130786588064317 - }, - { - "days": 0.435417, - "weight": 0.00014751599455829886 - }, - { - "days": 0.436806, - "weight": 4.917199818609962e-05 - }, - { - "days": 0.4375, - "weight": 0.0008268180435736752 - }, - { - "days": 0.438194, - "weight": 0.0002458599909304981 - }, - { - "days": 0.438889, - "weight": 0.0003023167295886125 - }, - { - "days": 0.440278, - "weight": 0.00016390666062033208 - }, - { - "days": 0.442361, - "weight": 0.00018758206715438003 - }, - { - "days": 0.443056, - "weight": 0.0001620854755023284 - }, - { - "days": 0.446528, - "weight": 0.00013112532849626567 - }, - { - "days": 0.447222, - "weight": 0.0003733429491907564 - }, - { - "days": 0.447917, - "weight": 0.0002276481397504612 - }, - { - "days": 0.449306, - "weight": 0.00017119140109234684 - }, - { - "days": 0.453472, - "weight": 0.00041523020690484124 - }, - { - "days": 0.457639, - "weight": 0.00040066072596081176 - }, - { - "days": 0.458333, - "weight": 0.00048443524138898147 - }, - { - "days": 0.459028, - "weight": 8.74168856641771e-05 - }, - { - "days": 0.460417, - "weight": 0.00024403880581249442 - }, - { - "days": 0.4625, - "weight": 0.001085426330330199 - }, - { - "days": 0.466667, - "weight": 0.002205455177902468 - }, - { - "days": 0.468056, - "weight": 0.00047715050091696673 - }, - { - "days": 0.470833, - "weight": 5.099318330410331e-05 - }, - { - "days": 0.472222, - "weight": 0.00021489984392443538 - }, - { - "days": 0.472917, - "weight": 0.0001948668076263948 - }, - { - "days": 0.473611, - "weight": 0.0005354284246930847 - }, - { - "days": 0.474306, - "weight": 0.000580958052643177 - }, - { - "days": 0.475, - "weight": 0.000316886210532642 - }, - { - "days": 0.477083, - "weight": 0.0010526449982061327 - }, - { - "days": 0.477778, - "weight": 0.0003533099128927158 - }, - { - "days": 0.479167, - "weight": 0.0005008259074510147 - }, - { - "days": 0.480556, - "weight": 0.0005481767205191106 - }, - { - "days": 0.48125, - "weight": 0.0006028122740592213 - }, - { - "days": 0.483333, - "weight": 0.00047168694556295565 - }, - { - "days": 0.484028, - "weight": 0.001462411649756963 - }, - { - "days": 0.484722, - "weight": 0.00032781332124066416 - }, - { - "days": 0.485417, - "weight": 0.000316886210532642 - }, - { - "days": 0.486806, - "weight": 0.001320359210552675 - }, - { - "days": 0.4875, - "weight": 0.0005937063484692029 - }, - { - "days": 0.488194, - "weight": 0.0003769853194267638 - }, - { - "days": 0.488889, - "weight": 0.0003314556914766715 - }, - { - "days": 0.490278, - "weight": 0.0008668841161697563 - }, - { - "days": 0.490972, - "weight": 0.0002531447314025129 - }, - { - "days": 0.491667, - "weight": 0.0004425479836748966 - }, - { - "days": 0.492361, - "weight": 0.0002822836932905719 - }, - { - "days": 0.493056, - "weight": 0.0008668841161697563 - }, - { - "days": 0.49375, - "weight": 0.0007011562704314205 - }, - { - "days": 0.494444, - "weight": 0.0038499853394598 - }, - { - "days": 0.495833, - "weight": 0.0004243361324948597 - }, - { - "days": 0.498611, - "weight": 0.0008195333031016604 - }, - { - "days": 0.499306, - "weight": 0.00028956843376258664 - }, - { - "days": 0.500694, - "weight": 0.0003223497658866531 - }, - { - "days": 0.502083, - "weight": 0.0003533099128927158 - }, - { - "days": 0.50625, - "weight": 0.0004061242813148228 - }, - { - "days": 0.506944, - "weight": 0.0008322815989276862 - }, - { - "days": 0.509028, - "weight": 0.0004061242813148228 - }, - { - "days": 0.511111, - "weight": 0.0012129092885904574 - }, - { - "days": 0.5125, - "weight": 0.00015297954991230994 - }, - { - "days": 0.517361, - "weight": 0.0004953623520970036 - }, - { - "days": 0.51875, - "weight": 0.00029138961888059035 - }, - { - "days": 0.521528, - "weight": 0.0006009910889412176 - }, - { - "days": 0.522917, - "weight": 0.00029685317423460144 - }, - { - "days": 0.524306, - "weight": 0.00042979968784887077 - }, - { - "days": 0.525, - "weight": 0.00020033036298040588 - }, - { - "days": 0.526389, - "weight": 0.00027864132305456454 - }, - { - "days": 0.527083, - "weight": 0.00020215154809840956 - }, - { - "days": 0.532639, - "weight": 0.00044801153902890767 - }, - { - "days": 0.533333, - "weight": 0.0008049638221576308 - }, - { - "days": 0.535417, - "weight": 0.0004953623520970036 - }, - { - "days": 0.536111, - "weight": 0.00033874043194868626 - }, - { - "days": 0.536806, - "weight": 0.0014988353521170367 - }, - { - "days": 0.538194, - "weight": 9.470162613619187e-05 - }, - { - "days": 0.539583, - "weight": 0.0006301300508292767 - }, - { - "days": 0.540278, - "weight": 0.0006009910889412176 - }, - { - "days": 0.543056, - "weight": 0.0005099318330410331 - }, - { - "days": 0.544444, - "weight": 0.0009433738911259113 - }, - { - "days": 0.545833, - "weight": 0.0004079454664328265 - }, - { - "days": 0.546528, - "weight": 0.0007849307858595903 - }, - { - "days": 0.547222, - "weight": 0.0003551310980107195 - }, - { - "days": 0.548611, - "weight": 0.00022400576951445383 - }, - { - "days": 0.549306, - "weight": 0.0003350980617126789 - }, - { - "days": 0.55, - "weight": 0.0015953581633712322 - }, - { - "days": 0.550694, - "weight": 0.0010599297386781473 - }, - { - "days": 0.551389, - "weight": 0.00045893864973692983 - }, - { - "days": 0.552083, - "weight": 0.00010744992196221769 - }, - { - "days": 0.552778, - "weight": 0.00021672102904243907 - }, - { - "days": 0.553472, - "weight": 0.0005627462014631402 - }, - { - "days": 0.554167, - "weight": 6.738384936613652e-05 - }, - { - "days": 0.554861, - "weight": 0.00028956843376258664 - }, - { - "days": 0.555556, - "weight": 0.00025678710163852027 - }, - { - "days": 0.556944, - "weight": 0.0001930456225083911 - }, - { - "days": 0.557639, - "weight": 0.0003241709510046568 - }, - { - "days": 0.558333, - "weight": 0.0003587734682467269 - }, - { - "days": 0.559722, - "weight": 0.00044072679855689293 - }, - { - "days": 0.560417, - "weight": 0.00011473466243423246 - }, - { - "days": 0.568056, - "weight": 0.00015115836479430626 - }, - { - "days": 0.570139, - "weight": 0.0005864216079971881 - }, - { - "days": 0.570833, - "weight": 9.470162613619187e-05 - }, - { - "days": 0.572917, - "weight": 0.00016390666062033208 - }, - { - "days": 0.575, - "weight": 0.00018393969691837266 - }, - { - "days": 0.58125, - "weight": 0.0002586082867565239 - }, - { - "days": 0.584722, - "weight": 0.0006975139001954132 - }, - { - "days": 0.5875, - "weight": 4.006607259608118e-05 - }, - { - "days": 0.590278, - "weight": 8.013214519216235e-05 - }, - { - "days": 0.591667, - "weight": 0.00040430309619681913 - }, - { - "days": 0.592361, - "weight": 0.0002312905099864686 - }, - { - "days": 0.593056, - "weight": 9.288044101818817e-05 - }, - { - "days": 0.59375, - "weight": 0.00012019821778824352 - }, - { - "days": 0.595833, - "weight": 0.00012930414337826196 - }, - { - "days": 0.597222, - "weight": 0.00027317776770055345 - }, - { - "days": 0.597917, - "weight": 0.0012110881034724536 - }, - { - "days": 0.598611, - "weight": 0.0004279785027308671 - }, - { - "days": 0.599306, - "weight": 9.652281125419556e-05 - }, - { - "days": 0.6, - "weight": 0.0001384100689682804 - }, - { - "days": 0.601389, - "weight": 0.0003023167295886125 - }, - { - "days": 0.602083, - "weight": 6.192029401212545e-05 - }, - { - "days": 0.602778, - "weight": 0.00021854221416044278 - }, - { - "days": 0.603472, - "weight": 0.0002276481397504612 - }, - { - "days": 0.605556, - "weight": 0.00048443524138898147 - }, - { - "days": 0.606944, - "weight": 0.00018211851180036898 - }, - { - "days": 0.607639, - "weight": 0.000158443105266321 - }, - { - "days": 0.609028, - "weight": 0.00016572784573833576 - }, - { - "days": 0.609722, - "weight": 0.00026042947187452763 - }, - { - "days": 0.610417, - "weight": 0.0001384100689682804 - }, - { - "days": 0.611111, - "weight": 0.0018940325227238372 - }, - { - "days": 0.611806, - "weight": 0.0003314556914766715 - }, - { - "days": 0.6125, - "weight": 7.10262196021439e-05 - }, - { - "days": 0.613194, - "weight": 5.4635553540110695e-05 - }, - { - "days": 0.613889, - "weight": 0.00031506502541463834 - }, - { - "days": 0.614583, - "weight": 0.0002859260635265793 - }, - { - "days": 0.615278, - "weight": 8.013214519216235e-05 - }, - { - "days": 0.615972, - "weight": 8.013214519216235e-05 - }, - { - "days": 0.616667, - "weight": 0.0003733429491907564 - }, - { - "days": 0.617361, - "weight": 0.00022946932486846491 - }, - { - "days": 0.61875, - "weight": 0.00019850917786240217 - }, - { - "days": 0.619444, - "weight": 9.105925590018449e-05 - }, - { - "days": 0.620139, - "weight": 0.0003296345063586678 - }, - { - "days": 0.622222, - "weight": 8.559570054617342e-05 - }, - { - "days": 0.623611, - "weight": 0.00010380755172621031 - }, - { - "days": 0.624306, - "weight": 0.00012019821778824352 - }, - { - "days": 0.628472, - "weight": 8.559570054617342e-05 - }, - { - "days": 0.631944, - "weight": 0.00027317776770055345 - }, - { - "days": 0.632639, - "weight": 8.74168856641771e-05 - }, - { - "days": 0.634028, - "weight": 0.0004552962795009224 - }, - { - "days": 0.642361, - "weight": 0.00011473466243423246 - }, - { - "days": 0.644444, - "weight": 0.0005536402758731216 - }, - { - "days": 0.648611, - "weight": 0.00011837703267023983 - }, - { - "days": 0.652778, - "weight": 0.001671847938327387 - }, - { - "days": 0.654167, - "weight": 0.0004680445753269483 - }, - { - "days": 0.654861, - "weight": 0.0001420524392042878 - }, - { - "days": 0.655556, - "weight": 0.00040430309619681913 - }, - { - "days": 0.65625, - "weight": 6.009910889412176e-05 - }, - { - "days": 0.656944, - "weight": 8.195333031016604e-05 - }, - { - "days": 0.657639, - "weight": 5.2814368422107004e-05 - }, - { - "days": 0.658333, - "weight": 0.00021672102904243907 - }, - { - "days": 0.659028, - "weight": 7.466858983815127e-05 - }, - { - "days": 0.660417, - "weight": 0.00024039643557648705 - }, - { - "days": 0.661806, - "weight": 0.0001802973266823653 - }, - { - "days": 0.6625, - "weight": 7.648977495615497e-05 - }, - { - "days": 0.663194, - "weight": 0.00011109229219822507 - }, - { - "days": 0.663889, - "weight": 6.009910889412176e-05 - }, - { - "days": 0.664583, - "weight": 0.0008923807078218079 - }, - { - "days": 0.665278, - "weight": 0.00012930414337826196 - }, - { - "days": 0.665972, - "weight": 0.0001802973266823653 - }, - { - "days": 0.666667, - "weight": 0.00011655584755223614 - }, - { - "days": 0.667361, - "weight": 0.000158443105266321 - }, - { - "days": 0.668056, - "weight": 4.188725771408486e-05 - }, - { - "days": 0.66875, - "weight": 0.0010471814428521216 - }, - { - "days": 0.669444, - "weight": 0.00021672102904243907 - }, - { - "days": 0.670139, - "weight": 0.0016937021597434314 - }, - { - "days": 0.670833, - "weight": 0.000158443105266321 - }, - { - "days": 0.671528, - "weight": 0.0001420524392042878 - }, - { - "days": 0.672917, - "weight": 0.0002476811760485018 - }, - { - "days": 0.673611, - "weight": 0.00021489984392443538 - }, - { - "days": 0.674306, - "weight": 0.00016937021597434313 - }, - { - "days": 0.675, - "weight": 7.831096007415866e-05 - }, - { - "days": 0.675694, - "weight": 0.0002513235462845092 - }, - { - "days": 0.676389, - "weight": 0.0001948668076263948 - }, - { - "days": 0.677083, - "weight": 0.00023311169510447228 - }, - { - "days": 0.678472, - "weight": 0.00010016518149020294 - }, - { - "days": 0.68125, - "weight": 6.009910889412176e-05 - }, - { - "days": 0.681944, - "weight": 0.0002822836932905719 - }, - { - "days": 0.682639, - "weight": 0.00012748295826025828 - }, - { - "days": 0.683333, - "weight": 0.0003915548003707933 - }, - { - "days": 0.684028, - "weight": 0.00045165390926491504 - }, - { - "days": 0.6875, - "weight": 0.00013476769873227304 - }, - { - "days": 0.688889, - "weight": 0.0001802973266823653 - }, - { - "days": 0.690278, - "weight": 5.645673865811438e-05 - }, - { - "days": 0.69375, - "weight": 0.00010380755172621031 - }, - { - "days": 0.695833, - "weight": 6.92050344841402e-05 - }, - { - "days": 0.698611, - "weight": 0.0009051290036478338 - }, - { - "days": 0.70625, - "weight": 3.278133212406642e-05 - }, - { - "days": 0.707639, - "weight": 0.00011473466243423246 - }, - { - "days": 0.713889, - "weight": 0.00043708442832088556 - }, - { - "days": 0.716667, - "weight": 0.0001620854755023284 - }, - { - "days": 0.71875, - "weight": 0.0008468510798717157 - }, - { - "days": 0.719444, - "weight": 0.0007794672305055792 - }, - { - "days": 0.720139, - "weight": 0.001537080239595114 - }, - { - "days": 0.720833, - "weight": 0.0003915548003707933 - }, - { - "days": 0.721528, - "weight": 0.00020579391833441693 - }, - { - "days": 0.722222, - "weight": 9.834399637219924e-05 - }, - { - "days": 0.723611, - "weight": 0.00021489984392443538 - }, - { - "days": 0.725, - "weight": 9.105925590018449e-05 - }, - { - "days": 0.725694, - "weight": 0.00012201940290624721 - }, - { - "days": 0.726389, - "weight": 0.0003788065045447675 - }, - { - "days": 0.727083, - "weight": 0.00011473466243423246 - }, - { - "days": 0.727778, - "weight": 0.0001566219201483173 - }, - { - "days": 0.728472, - "weight": 0.00024039643557648705 - }, - { - "days": 0.729167, - "weight": 0.00031142265517863097 - }, - { - "days": 0.729861, - "weight": 0.00011291347731622876 - }, - { - "days": 0.730556, - "weight": 0.0013749947640927858 - }, - { - "days": 0.73125, - "weight": 0.00010016518149020294 - }, - { - "days": 0.731944, - "weight": 7.648977495615497e-05 - }, - { - "days": 0.732639, - "weight": 0.0001402312540862841 - }, - { - "days": 0.733333, - "weight": 8.195333031016604e-05 - }, - { - "days": 0.734028, - "weight": 0.00026225065699253135 - }, - { - "days": 0.734722, - "weight": 9.288044101818817e-05 - }, - { - "days": 0.735417, - "weight": 0.00035695228312872316 - }, - { - "days": 0.736806, - "weight": 0.00010562873684421401 - }, - { - "days": 0.7375, - "weight": 0.00015297954991230994 - }, - { - "days": 0.738194, - "weight": 0.0007157257513754501 - }, - { - "days": 0.738889, - "weight": 0.0002768201379365608 - }, - { - "days": 0.740278, - "weight": 0.00014751599455829886 - }, - { - "days": 0.740972, - "weight": 6.374147913012914e-05 - }, - { - "days": 0.741667, - "weight": 0.00011291347731622876 - }, - { - "days": 0.743056, - "weight": 4.370844283208855e-05 - }, - { - "days": 0.744444, - "weight": 0.00010744992196221769 - }, - { - "days": 0.746528, - "weight": 0.00010380755172621031 - }, - { - "days": 0.747222, - "weight": 0.0005627462014631402 - }, - { - "days": 0.748611, - "weight": 0.00013658888385027673 - }, - { - "days": 0.749306, - "weight": 0.00015115836479430626 - }, - { - "days": 0.75, - "weight": 6.374147913012914e-05 - }, - { - "days": 0.750694, - "weight": 0.00012201940290624721 - }, - { - "days": 0.7625, - "weight": 7.284740472014759e-05 - }, - { - "days": 0.776389, - "weight": 0.00023857525045848336 - }, - { - "days": 0.777083, - "weight": 6.556266424813284e-05 - }, - { - "days": 0.777778, - "weight": 6.009910889412176e-05 - }, - { - "days": 0.778472, - "weight": 0.0005299648693390737 - }, - { - "days": 0.779167, - "weight": 0.00020215154809840956 - }, - { - "days": 0.779861, - "weight": 0.0025223413884351105 - }, - { - "days": 0.780556, - "weight": 0.00046986576044495194 - }, - { - "days": 0.78125, - "weight": 0.00011473466243423246 - }, - { - "days": 0.781944, - "weight": 0.00010198636660820662 - }, - { - "days": 0.782639, - "weight": 0.0003478463575387047 - }, - { - "days": 0.783333, - "weight": 9.834399637219924e-05 - }, - { - "days": 0.784028, - "weight": 0.00021489984392443538 - }, - { - "days": 0.785417, - "weight": 0.0004735081306809593 - }, - { - "days": 0.786806, - "weight": 0.000316886210532642 - }, - { - "days": 0.7875, - "weight": 0.002899326707861874 - }, - { - "days": 0.788194, - "weight": 0.0006173817550032509 - }, - { - "days": 0.788889, - "weight": 4.7350813068095936e-05 - }, - { - "days": 0.789583, - "weight": 0.0008177121179836567 - }, - { - "days": 0.790972, - "weight": 7.648977495615497e-05 - }, - { - "days": 0.791667, - "weight": 0.00036423702360073796 - }, - { - "days": 0.792361, - "weight": 0.0003205285807686494 - }, - { - "days": 0.793056, - "weight": 0.00010198636660820662 - }, - { - "days": 0.79375, - "weight": 0.0001420524392042878 - }, - { - "days": 0.795833, - "weight": 0.0004571174646189261 - }, - { - "days": 0.796528, - "weight": 0.0001620854755023284 - }, - { - "days": 0.797917, - "weight": 0.00019122443739038743 - }, - { - "days": 0.798611, - "weight": 0.0003951971706068007 - }, - { - "days": 0.799306, - "weight": 0.00010927110708022139 - }, - { - "days": 0.8, - "weight": 0.00020397273321641325 - }, - { - "days": 0.800694, - "weight": 6.92050344841402e-05 - }, - { - "days": 0.801389, - "weight": 0.00041158783666883387 - }, - { - "days": 0.802778, - "weight": 9.652281125419556e-05 - }, - { - "days": 0.804167, - "weight": 0.00012201940290624721 - }, - { - "days": 0.804861, - "weight": 0.0003733429491907564 - }, - { - "days": 0.805556, - "weight": 9.105925590018449e-05 - }, - { - "days": 0.80625, - "weight": 6.556266424813284e-05 - }, - { - "days": 0.808333, - "weight": 0.00010198636660820662 - }, - { - "days": 0.809028, - "weight": 0.00010380755172621031 - }, - { - "days": 0.810417, - "weight": 0.0003951971706068007 - }, - { - "days": 0.811111, - "weight": 0.0003023167295886125 - }, - { - "days": 0.827083, - "weight": 0.00011109229219822507 - }, - { - "days": 0.832639, - "weight": 0.00018940325227238374 - }, - { - "days": 0.836806, - "weight": 8.377451542816972e-05 - }, - { - "days": 0.838194, - "weight": 0.0003788065045447675 - }, - { - "days": 0.838889, - "weight": 0.0009761552232499777 - }, - { - "days": 0.839583, - "weight": 0.0005317860544570774 - }, - { - "days": 0.840278, - "weight": 0.00010198636660820662 - }, - { - "days": 0.840972, - "weight": 5.827792377611807e-05 - }, - { - "days": 0.841667, - "weight": 0.00024950236116650547 - }, - { - "days": 0.842361, - "weight": 0.00011655584755223614 - }, - { - "days": 0.84375, - "weight": 0.00037516413430876007 - }, - { - "days": 0.844444, - "weight": 0.00016572784573833576 - }, - { - "days": 0.845833, - "weight": 0.000287747248644583 - }, - { - "days": 0.846528, - "weight": 0.0007812884156235829 - }, - { - "days": 0.847222, - "weight": 0.0010380755172621032 - }, - { - "days": 0.849306, - "weight": 0.0031196901071403205 - }, - { - "days": 0.85, - "weight": 0.00019122443739038743 - }, - { - "days": 0.850694, - "weight": 0.0005408919800470958 - }, - { - "days": 0.851389, - "weight": 0.0002531447314025129 - }, - { - "days": 0.852778, - "weight": 8.377451542816972e-05 - }, - { - "days": 0.854167, - "weight": 9.652281125419556e-05 - }, - { - "days": 0.854861, - "weight": 0.0006082758294132323 - }, - { - "days": 0.855556, - "weight": 0.000264071842110535 - }, - { - "days": 0.856944, - "weight": 0.0003205285807686494 - }, - { - "days": 0.857639, - "weight": 0.00012019821778824352 - }, - { - "days": 0.858333, - "weight": 0.00010927110708022139 - }, - { - "days": 0.860417, - "weight": 0.00026225065699253135 - }, - { - "days": 0.861111, - "weight": 0.0005190377586310516 - }, - { - "days": 0.861806, - "weight": 0.00015480073503031363 - }, - { - "days": 0.863889, - "weight": 0.00042979968784887077 - }, - { - "days": 0.864583, - "weight": 0.00020943628857042433 - }, - { - "days": 0.865278, - "weight": 0.00033874043194868626 - }, - { - "days": 0.865972, - "weight": 0.00019668799274439848 - }, - { - "days": 0.866667, - "weight": 0.0001420524392042878 - }, - { - "days": 0.868056, - "weight": 0.00018393969691837266 - }, - { - "days": 0.86875, - "weight": 0.0003369192468306826 - }, - { - "days": 0.870139, - "weight": 0.0003023167295886125 - }, - { - "days": 0.870833, - "weight": 0.00027135658258254974 - }, - { - "days": 0.872222, - "weight": 0.00026042947187452763 - }, - { - "days": 0.882639, - "weight": 0.00012201940290624721 - }, - { - "days": 0.890278, - "weight": 6.374147913012914e-05 - }, - { - "days": 0.891667, - "weight": 0.00036241583848273425 - }, - { - "days": 0.896528, - "weight": 0.00052814368422107 - }, - { - "days": 0.897917, - "weight": 0.00012019821778824352 - }, - { - "days": 0.898611, - "weight": 0.00047715050091696673 - }, - { - "days": 0.901389, - "weight": 0.0002768201379365608 - }, - { - "days": 0.902083, - "weight": 0.0001238405880242509 - }, - { - "days": 0.902778, - "weight": 0.00025496591652051655 - }, - { - "days": 0.903472, - "weight": 0.00024221762069449073 - }, - { - "days": 0.904861, - "weight": 0.00037516413430876007 - }, - { - "days": 0.905556, - "weight": 0.004046673332204199 - }, - { - "days": 0.90625, - "weight": 0.0006374147913012914 - }, - { - "days": 0.906944, - "weight": 0.000657447827599332 - }, - { - "days": 0.907639, - "weight": 0.0004571174646189261 - }, - { - "days": 0.908333, - "weight": 0.0005153953883950442 - }, - { - "days": 0.909722, - "weight": 8.377451542816972e-05 - }, - { - "days": 0.913194, - "weight": 0.00020761510345242062 - }, - { - "days": 0.913889, - "weight": 0.00024950236116650547 - }, - { - "days": 0.914583, - "weight": 0.0004443691687929003 - }, - { - "days": 0.915278, - "weight": 0.0001256617731422546 - }, - { - "days": 0.915972, - "weight": 0.000921519669709867 - }, - { - "days": 0.916667, - "weight": 0.00044619035391090396 - }, - { - "days": 0.917361, - "weight": 3.8244887478077486e-05 - }, - { - "days": 0.918056, - "weight": 0.0002276481397504612 - }, - { - "days": 0.921528, - "weight": 0.011948795559222208 - }, - { - "days": 0.922222, - "weight": 0.00023857525045848336 - }, - { - "days": 0.922917, - "weight": 0.0009634069274239519 - }, - { - "days": 0.923611, - "weight": 0.00033327687659467524 - }, - { - "days": 0.924306, - "weight": 0.0027791284900736306 - }, - { - "days": 0.925, - "weight": 0.00021854221416044278 - }, - { - "days": 0.925694, - "weight": 0.0005153953883950442 - }, - { - "days": 0.926389, - "weight": 0.00011109229219822507 - }, - { - "days": 0.927083, - "weight": 0.0002130786588064317 - }, - { - "days": 0.930556, - "weight": 0.0006009910889412176 - }, - { - "days": 0.93125, - "weight": 0.0003551310980107195 - }, - { - "days": 0.931944, - "weight": 0.00019668799274439848 - }, - { - "days": 0.932639, - "weight": 0.00030960147006062726 - }, - { - "days": 0.933333, - "weight": 0.0003551310980107195 - }, - { - "days": 0.94375, - "weight": 0.0004735081306809593 - }, - { - "days": 0.948611, - "weight": 0.00024221762069449073 - }, - { - "days": 0.950694, - "weight": 0.00032781332124066416 - }, - { - "days": 0.951389, - "weight": 0.0013003261742546345 - }, - { - "days": 0.955556, - "weight": 0.0008286392286916788 - }, - { - "days": 0.95625, - "weight": 0.0029539622614019848 - }, - { - "days": 0.956944, - "weight": 0.001016221295846059 - }, - { - "days": 0.957639, - "weight": 0.0003478463575387047 - }, - { - "days": 0.958333, - "weight": 0.00023675406534047965 - }, - { - "days": 0.959028, - "weight": 0.0006009910889412176 - }, - { - "days": 0.959722, - "weight": 6.92050344841402e-05 - }, - { - "days": 0.960417, - "weight": 0.0036369066806533684 - }, - { - "days": 0.961111, - "weight": 0.001819363932885686 - }, - { - "days": 0.961806, - "weight": 0.00023857525045848336 - }, - { - "days": 0.9625, - "weight": 0.00139684898550883 - }, - { - "days": 0.964583, - "weight": 0.0011345983285162987 - }, - { - "days": 0.965278, - "weight": 0.0005245013139850627 - }, - { - "days": 0.965972, - "weight": 0.0015862522377812138 - }, - { - "days": 0.968056, - "weight": 0.0009124137441198486 - }, - { - "days": 0.96875, - "weight": 0.007162721069108512 - }, - { - "days": 0.970139, - "weight": 0.0004971835372150073 - }, - { - "days": 0.972222, - "weight": 0.0018047944519416566 - }, - { - "days": 0.972917, - "weight": 0.008789039379485806 - }, - { - "days": 0.973611, - "weight": 0.0015589344610111584 - }, - { - "days": 0.974306, - "weight": 0.00161903356990528 - }, - { - "days": 0.975, - "weight": 0.0007776460453875755 - }, - { - "days": 0.975694, - "weight": 0.00025496591652051655 - }, - { - "days": 0.976389, - "weight": 0.0009579433720699408 - }, - { - "days": 0.977083, - "weight": 0.004013892000080132 - }, - { - "days": 0.977778, - "weight": 4.7350813068095936e-05 - }, - { - "days": 0.979167, - "weight": 0.001719198751395483 - }, - { - "days": 0.979861, - "weight": 0.0008778112268777784 - }, - { - "days": 0.980556, - "weight": 0.0013076109147266493 - }, - { - "days": 0.98125, - "weight": 0.0017501588984015458 - }, - { - "days": 0.981944, - "weight": 0.003964720001894032 - }, - { - "days": 0.982639, - "weight": 0.005221337733316579 - }, - { - "days": 0.983333, - "weight": 0.0012165516588264647 - }, - { - "days": 0.984722, - "weight": 0.00041705139202284495 - }, - { - "days": 0.985417, - "weight": 0.0009561221869519371 - }, - { - "days": 0.986111, - "weight": 0.008585066646269393 - }, - { - "days": 0.986806, - "weight": 0.0003296345063586678 - }, - { - "days": 0.988194, - "weight": 0.010790521824171862 - }, - { - "days": 0.988889, - "weight": 0.001132777143398295 - }, - { - "days": 0.989583, - "weight": 0.0004279785027308671 - }, - { - "days": 0.990278, - "weight": 0.0018776418566618042 - }, - { - "days": 0.990972, - "weight": 0.003469357649797029 - }, - { - "days": 0.991667, - "weight": 4.188725771408486e-05 - }, - { - "days": 0.995139, - "weight": 0.001150988994578332 - }, - { - "days": 0.995833, - "weight": 0.00014751599455829886 - }, - { - "days": 0.998611, - "weight": 0.0005682097568171512 - }, - { - "days": 1.002778, - "weight": 0.0004279785027308671 - }, - { - "days": 1.007639, - "weight": 0.001544364980067129 - }, - { - "days": 1.008333, - "weight": 4.7350813068095936e-05 - }, - { - "days": 1.009722, - "weight": 0.003493033056331077 - }, - { - "days": 1.011111, - "weight": 0.0004261573176128634 - }, - { - "days": 1.014583, - "weight": 9.105925590018449e-05 - }, - { - "days": 1.015278, - "weight": 0.002493202426547051 - }, - { - "days": 1.016667, - "weight": 0.002449493983714963 - }, - { - "days": 1.017361, - "weight": 0.0018794630417798077 - }, - { - "days": 1.018056, - "weight": 0.022730211457804052 - }, - { - "days": 1.01875, - "weight": 0.0021034688112942616 - }, - { - "days": 1.020139, - "weight": 0.0006283088657112729 - }, - { - "days": 1.021528, - "weight": 0.00019122443739038743 - }, - { - "days": 1.022917, - "weight": 0.0015807886824272027 - }, - { - "days": 1.023611, - "weight": 0.00022036339927844646 - }, - { - "days": 1.024306, - "weight": 0.0004243361324948597 - }, - { - "days": 1.025, - "weight": 0.0002804625081725682 - }, - { - "days": 1.027083, - "weight": 0.0008304604138096825 - }, - { - "days": 1.027778, - "weight": 0.005159417439304453 - }, - { - "days": 1.028472, - "weight": 0.00010927110708022139 - }, - { - "days": 1.029861, - "weight": 0.0021617467350703795 - }, - { - "days": 1.03125, - "weight": 0.00024039643557648705 - }, - { - "days": 1.031944, - "weight": 0.0024458516134789553 - }, - { - "days": 1.032639, - "weight": 0.002223667029082505 - }, - { - "days": 1.033333, - "weight": 0.0002677142123465424 - }, - { - "days": 1.034028, - "weight": 0.001338571061732712 - }, - { - "days": 1.034722, - "weight": 0.005957096520990069 - }, - { - "days": 1.035417, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.036111, - "weight": 0.001415060836688867 - }, - { - "days": 1.036806, - "weight": 0.00028956843376258664 - }, - { - "days": 1.038889, - "weight": 0.000921519669709867 - }, - { - "days": 1.039583, - "weight": 0.0011764855862303836 - }, - { - "days": 1.040972, - "weight": 0.000604633459177225 - }, - { - "days": 1.041667, - "weight": 0.0006501630871273172 - }, - { - "days": 1.042361, - "weight": 0.0014696963902289775 - }, - { - "days": 1.043056, - "weight": 0.002342044061752745 - }, - { - "days": 1.04375, - "weight": 0.0013768159492107894 - }, - { - "days": 1.044444, - "weight": 0.0005208589437490553 - }, - { - "days": 1.045139, - "weight": 0.0016955233448614352 - }, - { - "days": 1.045833, - "weight": 0.000686586789487391 - }, - { - "days": 1.046528, - "weight": 0.0008450298947537121 - }, - { - "days": 1.048611, - "weight": 0.000662911382953343 - }, - { - "days": 1.05, - "weight": 0.0007175469364934538 - }, - { - "days": 1.050694, - "weight": 0.0005299648693390737 - }, - { - "days": 1.051389, - "weight": 0.025820762603056314 - }, - { - "days": 1.052083, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.052778, - "weight": 5.645673865811438e-05 - }, - { - "days": 1.054861, - "weight": 0.003372834838542833 - }, - { - "days": 1.056944, - "weight": 0.0003842700598987785 - }, - { - "days": 1.058333, - "weight": 0.0005827792377611807 - }, - { - "days": 1.064583, - "weight": 0.00028956843376258664 - }, - { - "days": 1.065972, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.069444, - "weight": 0.0005172165735130478 - }, - { - "days": 1.073611, - "weight": 0.00043708442832088556 - }, - { - "days": 1.074306, - "weight": 0.0001438736243222915 - }, - { - "days": 1.076389, - "weight": 0.0003897336152527896 - }, - { - "days": 1.077083, - "weight": 0.00034420398730269734 - }, - { - "days": 1.077778, - "weight": 0.00021854221416044278 - }, - { - "days": 1.079167, - "weight": 0.00010016518149020294 - }, - { - "days": 1.079861, - "weight": 0.00018576088203637635 - }, - { - "days": 1.08125, - "weight": 0.00012201940290624721 - }, - { - "days": 1.082639, - "weight": 0.000686586789487391 - }, - { - "days": 1.084028, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.084722, - "weight": 0.0017465165281655385 - }, - { - "days": 1.086111, - "weight": 0.0002531447314025129 - }, - { - "days": 1.0875, - "weight": 8.559570054617342e-05 - }, - { - "days": 1.089583, - "weight": 0.0004024819110788154 - }, - { - "days": 1.090972, - "weight": 0.00023311169510447228 - }, - { - "days": 1.091667, - "weight": 0.0009670492976599593 - }, - { - "days": 1.092361, - "weight": 0.0014642328348749665 - }, - { - "days": 1.09375, - "weight": 0.0006119181996492398 - }, - { - "days": 1.094444, - "weight": 0.0002130786588064317 - }, - { - "days": 1.095139, - "weight": 0.00030960147006062726 - }, - { - "days": 1.097917, - "weight": 0.0017119140109234684 - }, - { - "days": 1.098611, - "weight": 0.00011655584755223614 - }, - { - "days": 1.1, - "weight": 0.00019122443739038743 - }, - { - "days": 1.100694, - "weight": 0.0005463555354011069 - }, - { - "days": 1.101389, - "weight": 0.0005226801288670589 - }, - { - "days": 1.102083, - "weight": 0.00044619035391090396 - }, - { - "days": 1.104167, - "weight": 0.00026225065699253135 - }, - { - "days": 1.104861, - "weight": 0.00018758206715438003 - }, - { - "days": 1.105556, - "weight": 9.288044101818817e-05 - }, - { - "days": 1.10625, - "weight": 0.0002658930272285387 - }, - { - "days": 1.108333, - "weight": 0.0002695353974645461 - }, - { - "days": 1.109028, - "weight": 8.74168856641771e-05 - }, - { - "days": 1.109722, - "weight": 9.834399637219924e-05 - }, - { - "days": 1.110417, - "weight": 0.00028956843376258664 - }, - { - "days": 1.111806, - "weight": 0.00023675406534047965 - }, - { - "days": 1.1125, - "weight": 0.0002859260635265793 - }, - { - "days": 1.113889, - "weight": 0.00017301258621035053 - }, - { - "days": 1.115972, - "weight": 9.288044101818817e-05 - }, - { - "days": 1.116667, - "weight": 0.00016572784573833576 - }, - { - "days": 1.118056, - "weight": 0.000575494497289166 - }, - { - "days": 1.120139, - "weight": 0.0005336072395750811 - }, - { - "days": 1.120833, - "weight": 0.011540850092789382 - }, - { - "days": 1.131944, - "weight": 8.377451542816972e-05 - }, - { - "days": 1.136111, - "weight": 0.00020761510345242062 - }, - { - "days": 1.136806, - "weight": 0.00025496591652051655 - }, - { - "days": 1.1375, - "weight": 0.00020215154809840956 - }, - { - "days": 1.138889, - "weight": 0.0004990047223330109 - }, - { - "days": 1.140278, - "weight": 5.645673865811438e-05 - }, - { - "days": 1.141667, - "weight": 0.00010927110708022139 - }, - { - "days": 1.145833, - "weight": 0.00014933717967630255 - }, - { - "days": 1.146528, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.147222, - "weight": 0.00012019821778824352 - }, - { - "days": 1.147917, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.148611, - "weight": 9.105925590018449e-05 - }, - { - "days": 1.149306, - "weight": 0.0001238405880242509 - }, - { - "days": 1.15, - "weight": 0.0001438736243222915 - }, - { - "days": 1.150694, - "weight": 0.0003660582087187416 - }, - { - "days": 1.151389, - "weight": 0.0006392359764192951 - }, - { - "days": 1.152083, - "weight": 0.00013112532849626567 - }, - { - "days": 1.153472, - "weight": 0.0001602642903843247 - }, - { - "days": 1.154861, - "weight": 0.0001420524392042878 - }, - { - "days": 1.155556, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.156944, - "weight": 5.2814368422107004e-05 - }, - { - "days": 1.157639, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.159028, - "weight": 0.0004862564265069852 - }, - { - "days": 1.159722, - "weight": 0.00048807761162498884 - }, - { - "days": 1.160417, - "weight": 0.0015243319437690884 - }, - { - "days": 1.161111, - "weight": 0.00029138961888059035 - }, - { - "days": 1.161806, - "weight": 7.466858983815127e-05 - }, - { - "days": 1.163194, - "weight": 0.0001566219201483173 - }, - { - "days": 1.163889, - "weight": 0.00036970057895474904 - }, - { - "days": 1.165278, - "weight": 0.00033327687659467524 - }, - { - "days": 1.165972, - "weight": 5.4635553540110695e-05 - }, - { - "days": 1.166667, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.167361, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.168056, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.170139, - "weight": 8.559570054617342e-05 - }, - { - "days": 1.170833, - "weight": 0.00013112532849626567 - }, - { - "days": 1.171528, - "weight": 0.00016572784573833576 - }, - { - "days": 1.172917, - "weight": 6.738384936613652e-05 - }, - { - "days": 1.174306, - "weight": 0.00029321080399859407 - }, - { - "days": 1.176389, - "weight": 0.00023675406534047965 - }, - { - "days": 1.177083, - "weight": 0.00020215154809840956 - }, - { - "days": 1.178472, - "weight": 0.00011473466243423246 - }, - { - "days": 1.181944, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.184722, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.1875, - "weight": 0.0001948668076263948 - }, - { - "days": 1.189583, - "weight": 3.096014700606273e-05 - }, - { - "days": 1.191667, - "weight": 0.00010016518149020294 - }, - { - "days": 1.193056, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.19375, - "weight": 0.0001802973266823653 - }, - { - "days": 1.194444, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.198611, - "weight": 0.00010016518149020294 - }, - { - "days": 1.199306, - "weight": 0.0019869129637420254 - }, - { - "days": 1.200694, - "weight": 4.5529627950092245e-05 - }, - { - "days": 1.202778, - "weight": 0.00011109229219822507 - }, - { - "days": 1.203472, - "weight": 6.738384936613652e-05 - }, - { - "days": 1.204861, - "weight": 0.00010380755172621031 - }, - { - "days": 1.205556, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.206944, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.209028, - "weight": 0.0001948668076263948 - }, - { - "days": 1.209722, - "weight": 9.834399637219924e-05 - }, - { - "days": 1.210417, - "weight": 0.0005172165735130478 - }, - { - "days": 1.211111, - "weight": 5.4635553540110695e-05 - }, - { - "days": 1.2125, - "weight": 0.0002822836932905719 - }, - { - "days": 1.213194, - "weight": 0.00011655584755223614 - }, - { - "days": 1.213889, - "weight": 0.0002130786588064317 - }, - { - "days": 1.214583, - "weight": 0.00015115836479430626 - }, - { - "days": 1.215278, - "weight": 0.00013476769873227304 - }, - { - "days": 1.215972, - "weight": 5.2814368422107004e-05 - }, - { - "days": 1.218056, - "weight": 0.00015480073503031363 - }, - { - "days": 1.219444, - "weight": 0.0001438736243222915 - }, - { - "days": 1.220139, - "weight": 4.7350813068095936e-05 - }, - { - "days": 1.220833, - "weight": 0.0008814535971137859 - }, - { - "days": 1.222222, - "weight": 0.0003769853194267638 - }, - { - "days": 1.222917, - "weight": 7.466858983815127e-05 - }, - { - "days": 1.224306, - "weight": 0.0001438736243222915 - }, - { - "days": 1.225694, - "weight": 0.0010836051452121954 - }, - { - "days": 1.226389, - "weight": 0.0001238405880242509 - }, - { - "days": 1.228472, - "weight": 0.00015115836479430626 - }, - { - "days": 1.229167, - "weight": 5.645673865811438e-05 - }, - { - "days": 1.229861, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.231944, - "weight": 0.00016937021597434313 - }, - { - "days": 1.232639, - "weight": 0.00010562873684421401 - }, - { - "days": 1.233333, - "weight": 0.00013658888385027673 - }, - { - "days": 1.234028, - "weight": 0.0001748337713283542 - }, - { - "days": 1.234722, - "weight": 8.923807078218079e-05 - }, - { - "days": 1.2375, - "weight": 0.0004735081306809593 - }, - { - "days": 1.238194, - "weight": 9.288044101818817e-05 - }, - { - "days": 1.238889, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.240278, - "weight": 0.00018393969691837266 - }, - { - "days": 1.246528, - "weight": 0.0019504892613819516 - }, - { - "days": 1.251389, - "weight": 7.831096007415866e-05 - }, - { - "days": 1.254167, - "weight": 0.0001930456225083911 - }, - { - "days": 1.25625, - "weight": 0.00012748295826025828 - }, - { - "days": 1.258333, - "weight": 0.00013294651361426936 - }, - { - "days": 1.261806, - "weight": 0.0002859260635265793 - }, - { - "days": 1.263889, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.264583, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.265972, - "weight": 0.00012201940290624721 - }, - { - "days": 1.267361, - "weight": 0.00027135658258254974 - }, - { - "days": 1.268056, - "weight": 0.00021489984392443538 - }, - { - "days": 1.26875, - "weight": 0.0001402312540862841 - }, - { - "days": 1.270139, - "weight": 0.00010016518149020294 - }, - { - "days": 1.272222, - "weight": 0.00011473466243423246 - }, - { - "days": 1.273611, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.274306, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.275, - "weight": 0.00015480073503031363 - }, - { - "days": 1.276389, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.277083, - "weight": 0.00017119140109234684 - }, - { - "days": 1.278472, - "weight": 0.00010562873684421401 - }, - { - "days": 1.279167, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.279861, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.281944, - "weight": 4.5529627950092245e-05 - }, - { - "days": 1.282639, - "weight": 0.00035148872777471214 - }, - { - "days": 1.283333, - "weight": 0.00012201940290624721 - }, - { - "days": 1.284028, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.284722, - "weight": 0.0004243361324948597 - }, - { - "days": 1.285417, - "weight": 0.000158443105266321 - }, - { - "days": 1.286111, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.288194, - "weight": 0.0003660582087187416 - }, - { - "days": 1.290278, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.29375, - "weight": 0.00015297954991230994 - }, - { - "days": 1.294444, - "weight": 0.00013112532849626567 - }, - { - "days": 1.295139, - "weight": 4.7350813068095936e-05 - }, - { - "days": 1.295833, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.296528, - "weight": 0.00018940325227238374 - }, - { - "days": 1.297917, - "weight": 0.00013112532849626567 - }, - { - "days": 1.298611, - "weight": 0.0005882427931151918 - }, - { - "days": 1.299306, - "weight": 0.00016937021597434313 - }, - { - "days": 1.303472, - "weight": 0.0002841048784085756 - }, - { - "days": 1.306944, - "weight": 0.00011837703267023983 - }, - { - "days": 1.313194, - "weight": 0.00010744992196221769 - }, - { - "days": 1.315972, - "weight": 7.466858983815127e-05 - }, - { - "days": 1.316667, - "weight": 5.645673865811438e-05 - }, - { - "days": 1.317361, - "weight": 5.2814368422107004e-05 - }, - { - "days": 1.322917, - "weight": 0.00010744992196221769 - }, - { - "days": 1.325, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.325694, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.326389, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.327083, - "weight": 0.00010380755172621031 - }, - { - "days": 1.327778, - "weight": 3.46025172420701e-05 - }, - { - "days": 1.328472, - "weight": 0.00018393969691837266 - }, - { - "days": 1.329167, - "weight": 5.645673865811438e-05 - }, - { - "days": 1.329861, - "weight": 0.00010380755172621031 - }, - { - "days": 1.330556, - "weight": 0.00011291347731622876 - }, - { - "days": 1.33125, - "weight": 0.0007266528620834721 - }, - { - "days": 1.332639, - "weight": 0.0001238405880242509 - }, - { - "days": 1.333333, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.334722, - "weight": 0.00013658888385027673 - }, - { - "days": 1.336806, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.338194, - "weight": 0.0001238405880242509 - }, - { - "days": 1.338889, - "weight": 0.00010016518149020294 - }, - { - "days": 1.339583, - "weight": 9.470162613619187e-05 - }, - { - "days": 1.340278, - "weight": 5.645673865811438e-05 - }, - { - "days": 1.340972, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.342361, - "weight": 0.00040430309619681913 - }, - { - "days": 1.343056, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.344444, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.345139, - "weight": 0.00013112532849626567 - }, - { - "days": 1.346528, - "weight": 0.00021854221416044278 - }, - { - "days": 1.348611, - "weight": 0.00023857525045848336 - }, - { - "days": 1.35, - "weight": 0.00020943628857042433 - }, - { - "days": 1.352083, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.352778, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.353472, - "weight": 0.00010744992196221769 - }, - { - "days": 1.354167, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.354861, - "weight": 6.92050344841402e-05 - }, - { - "days": 1.355556, - "weight": 0.00011109229219822507 - }, - { - "days": 1.357639, - "weight": 0.00016572784573833576 - }, - { - "days": 1.360417, - "weight": 0.000580958052643177 - }, - { - "days": 1.361111, - "weight": 0.00011837703267023983 - }, - { - "days": 1.363194, - "weight": 0.0001256617731422546 - }, - { - "days": 1.372222, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.372917, - "weight": 0.00012930414337826196 - }, - { - "days": 1.377083, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.379861, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.382639, - "weight": 0.00010380755172621031 - }, - { - "days": 1.385417, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.386111, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.386806, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.388194, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.388889, - "weight": 4.7350813068095936e-05 - }, - { - "days": 1.389583, - "weight": 9.288044101818817e-05 - }, - { - "days": 1.390972, - "weight": 7.466858983815127e-05 - }, - { - "days": 1.391667, - "weight": 0.00011473466243423246 - }, - { - "days": 1.392361, - "weight": 0.0001402312540862841 - }, - { - "days": 1.393056, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.395139, - "weight": 4.5529627950092245e-05 - }, - { - "days": 1.396528, - "weight": 3.46025172420701e-05 - }, - { - "days": 1.397222, - "weight": 0.0003205285807686494 - }, - { - "days": 1.397917, - "weight": 5.4635553540110695e-05 - }, - { - "days": 1.398611, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.399306, - "weight": 0.0004607598348549335 - }, - { - "days": 1.4, - "weight": 8.923807078218079e-05 - }, - { - "days": 1.402083, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.402778, - "weight": 3.096014700606273e-05 - }, - { - "days": 1.403472, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.404167, - "weight": 0.0003369192468306826 - }, - { - "days": 1.405556, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.407639, - "weight": 0.00012201940290624721 - }, - { - "days": 1.409028, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.409722, - "weight": 3.46025172420701e-05 - }, - { - "days": 1.410417, - "weight": 5.4635553540110695e-05 - }, - { - "days": 1.4125, - "weight": 0.00016572784573833576 - }, - { - "days": 1.413194, - "weight": 0.0001256617731422546 - }, - { - "days": 1.414583, - "weight": 0.0002476811760485018 - }, - { - "days": 1.415972, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.418056, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.41875, - "weight": 6.009910889412176e-05 - }, - { - "days": 1.419444, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.420139, - "weight": 0.00038244887478077486 - }, - { - "days": 1.426389, - "weight": 0.00012019821778824352 - }, - { - "days": 1.430556, - "weight": 9.834399637219924e-05 - }, - { - "days": 1.435417, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.4375, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.440278, - "weight": 4.7350813068095936e-05 - }, - { - "days": 1.441667, - "weight": 4.5529627950092245e-05 - }, - { - "days": 1.445833, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.447222, - "weight": 4.7350813068095936e-05 - }, - { - "days": 1.448611, - "weight": 0.000316886210532642 - }, - { - "days": 1.449306, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.45, - "weight": 5.645673865811438e-05 - }, - { - "days": 1.451389, - "weight": 0.000158443105266321 - }, - { - "days": 1.453472, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.454167, - "weight": 0.00022036339927844646 - }, - { - "days": 1.45625, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.456944, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.458333, - "weight": 0.00014569480944029518 - }, - { - "days": 1.460417, - "weight": 0.00032781332124066416 - }, - { - "days": 1.461111, - "weight": 0.00029321080399859407 - }, - { - "days": 1.463889, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.464583, - "weight": 8.74168856641771e-05 - }, - { - "days": 1.465972, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.468056, - "weight": 6.92050344841402e-05 - }, - { - "days": 1.469444, - "weight": 6.92050344841402e-05 - }, - { - "days": 1.470139, - "weight": 0.00023675406534047965 - }, - { - "days": 1.471528, - "weight": 0.00015115836479430626 - }, - { - "days": 1.472222, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.472917, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.474306, - "weight": 6.009910889412176e-05 - }, - { - "days": 1.475694, - "weight": 8.377451542816972e-05 - }, - { - "days": 1.476389, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.477778, - "weight": 0.00010380755172621031 - }, - { - "days": 1.479167, - "weight": 4.5529627950092245e-05 - }, - { - "days": 1.48125, - "weight": 0.00042979968784887077 - }, - { - "days": 1.482639, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.495833, - "weight": 7.466858983815127e-05 - }, - { - "days": 1.498611, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.499306, - "weight": 0.0002130786588064317 - }, - { - "days": 1.504167, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.507639, - "weight": 9.470162613619187e-05 - }, - { - "days": 1.508333, - "weight": 9.288044101818817e-05 - }, - { - "days": 1.509028, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.509722, - "weight": 0.00010744992196221769 - }, - { - "days": 1.510417, - "weight": 3.46025172420701e-05 - }, - { - "days": 1.513194, - "weight": 0.00012019821778824352 - }, - { - "days": 1.513889, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.515278, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.518056, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.51875, - "weight": 4.5529627950092245e-05 - }, - { - "days": 1.519444, - "weight": 0.00012201940290624721 - }, - { - "days": 1.520139, - "weight": 0.0004935411669789999 - }, - { - "days": 1.521528, - "weight": 0.00011291347731622876 - }, - { - "days": 1.522222, - "weight": 0.0001748337713283542 - }, - { - "days": 1.523611, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.524306, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.527083, - "weight": 0.00013658888385027673 - }, - { - "days": 1.527778, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.528472, - "weight": 7.831096007415866e-05 - }, - { - "days": 1.529167, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.530556, - "weight": 4.7350813068095936e-05 - }, - { - "days": 1.53125, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.532639, - "weight": 0.00013476769873227304 - }, - { - "days": 1.534028, - "weight": 7.466858983815127e-05 - }, - { - "days": 1.534722, - "weight": 8.74168856641771e-05 - }, - { - "days": 1.535417, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.536111, - "weight": 7.831096007415866e-05 - }, - { - "days": 1.536806, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.538194, - "weight": 0.00045893864973692983 - }, - { - "days": 1.545833, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.550694, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.55625, - "weight": 4.5529627950092245e-05 - }, - { - "days": 1.558333, - "weight": 0.0004316208729668745 - }, - { - "days": 1.564583, - "weight": 3.8244887478077486e-05 - }, - { - "days": 1.565972, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.567361, - "weight": 5.645673865811438e-05 - }, - { - "days": 1.568056, - "weight": 4.5529627950092245e-05 - }, - { - "days": 1.570139, - "weight": 4.5529627950092245e-05 - }, - { - "days": 1.572917, - "weight": 0.00014933717967630255 - }, - { - "days": 1.574306, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.577778, - "weight": 4.7350813068095936e-05 - }, - { - "days": 1.578472, - "weight": 5.2814368422107004e-05 - }, - { - "days": 1.579167, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.580556, - "weight": 0.00015480073503031363 - }, - { - "days": 1.581944, - "weight": 0.0005062894628050258 - }, - { - "days": 1.582639, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.583333, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.585417, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.586806, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.5875, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.588194, - "weight": 0.0001602642903843247 - }, - { - "days": 1.590972, - "weight": 0.00010562873684421401 - }, - { - "days": 1.592361, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.593056, - "weight": 9.105925590018449e-05 - }, - { - "days": 1.594444, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.595833, - "weight": 0.0004134090217868376 - }, - { - "days": 1.596528, - "weight": 7.831096007415866e-05 - }, - { - "days": 1.597917, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.599306, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.6, - "weight": 3.46025172420701e-05 - }, - { - "days": 1.601389, - "weight": 3.096014700606273e-05 - }, - { - "days": 1.615278, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.621528, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.625, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.627083, - "weight": 3.8244887478077486e-05 - }, - { - "days": 1.634722, - "weight": 0.00010562873684421401 - }, - { - "days": 1.6375, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.639583, - "weight": 7.466858983815127e-05 - }, - { - "days": 1.640972, - "weight": 7.831096007415866e-05 - }, - { - "days": 1.641667, - "weight": 3.46025172420701e-05 - }, - { - "days": 1.642361, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.64375, - "weight": 0.00017119140109234684 - }, - { - "days": 1.644444, - "weight": 9.105925590018449e-05 - }, - { - "days": 1.645833, - "weight": 6.92050344841402e-05 - }, - { - "days": 1.646528, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.647917, - "weight": 0.00022400576951445383 - }, - { - "days": 1.650694, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.653472, - "weight": 0.00010198636660820662 - }, - { - "days": 1.654861, - "weight": 0.00022400576951445383 - }, - { - "days": 1.655556, - "weight": 0.00016754903085633945 - }, - { - "days": 1.65625, - "weight": 6.009910889412176e-05 - }, - { - "days": 1.656944, - "weight": 0.00012201940290624721 - }, - { - "days": 1.658333, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.659722, - "weight": 0.0004807928711529741 - }, - { - "days": 1.6625, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.668056, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.670833, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.671528, - "weight": 0.0001402312540862841 - }, - { - "days": 1.676389, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.684028, - "weight": 3.46025172420701e-05 - }, - { - "days": 1.689583, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.691667, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.69375, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.696528, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.697917, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.698611, - "weight": 3.8244887478077486e-05 - }, - { - "days": 1.699306, - "weight": 9.470162613619187e-05 - }, - { - "days": 1.700694, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.703472, - "weight": 0.00010744992196221769 - }, - { - "days": 1.704861, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.705556, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.709722, - "weight": 0.00019850917786240217 - }, - { - "days": 1.711806, - "weight": 0.00012201940290624721 - }, - { - "days": 1.7125, - "weight": 5.4635553540110695e-05 - }, - { - "days": 1.713889, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.714583, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.715278, - "weight": 3.46025172420701e-05 - }, - { - "days": 1.715972, - "weight": 0.00010380755172621031 - }, - { - "days": 1.716667, - "weight": 0.00019122443739038743 - }, - { - "days": 1.71875, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.720139, - "weight": 0.000792215526331605 - }, - { - "days": 1.721528, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.731944, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.735417, - "weight": 0.00011655584755223614 - }, - { - "days": 1.738889, - "weight": 7.466858983815127e-05 - }, - { - "days": 1.743056, - "weight": 3.8244887478077486e-05 - }, - { - "days": 1.744444, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.748611, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.757639, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.758333, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.760417, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.761111, - "weight": 8.74168856641771e-05 - }, - { - "days": 1.7625, - "weight": 0.00021672102904243907 - }, - { - "days": 1.763194, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.766667, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.772222, - "weight": 8.74168856641771e-05 - }, - { - "days": 1.773611, - "weight": 0.00014569480944029518 - }, - { - "days": 1.775, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.775694, - "weight": 0.00010198636660820662 - }, - { - "days": 1.777083, - "weight": 9.652281125419556e-05 - }, - { - "days": 1.777778, - "weight": 0.00010016518149020294 - }, - { - "days": 1.779167, - "weight": 9.834399637219924e-05 - }, - { - "days": 1.781944, - "weight": 0.00020033036298040588 - }, - { - "days": 1.782639, - "weight": 0.0006793020490153763 - }, - { - "days": 1.789583, - "weight": 0.00012748295826025828 - }, - { - "days": 1.810417, - "weight": 6.009910889412176e-05 - }, - { - "days": 1.813194, - "weight": 3.8244887478077486e-05 - }, - { - "days": 1.815278, - "weight": 4.006607259608118e-05 - }, - { - "days": 1.819444, - "weight": 0.00011109229219822507 - }, - { - "days": 1.820833, - "weight": 3.8244887478077486e-05 - }, - { - "days": 1.821528, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.823611, - "weight": 9.105925590018449e-05 - }, - { - "days": 1.825, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.825694, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.827083, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.828472, - "weight": 9.834399637219924e-05 - }, - { - "days": 1.829167, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.83125, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.831944, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.834028, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.834722, - "weight": 0.00011291347731622876 - }, - { - "days": 1.835417, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.836806, - "weight": 0.00020397273321641325 - }, - { - "days": 1.8375, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.838889, - "weight": 8.74168856641771e-05 - }, - { - "days": 1.839583, - "weight": 5.2814368422107004e-05 - }, - { - "days": 1.840278, - "weight": 9.288044101818817e-05 - }, - { - "days": 1.840972, - "weight": 0.000158443105266321 - }, - { - "days": 1.844444, - "weight": 0.0007047986406674279 - }, - { - "days": 1.845833, - "weight": 4.7350813068095936e-05 - }, - { - "days": 1.847917, - "weight": 8.377451542816972e-05 - }, - { - "days": 1.849306, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.85625, - "weight": 9.834399637219924e-05 - }, - { - "days": 1.857639, - "weight": 3.8244887478077486e-05 - }, - { - "days": 1.863889, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.867361, - "weight": 3.8244887478077486e-05 - }, - { - "days": 1.868056, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.872222, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.872917, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.878472, - "weight": 0.00011655584755223614 - }, - { - "days": 1.880556, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.88125, - "weight": 5.4635553540110695e-05 - }, - { - "days": 1.882639, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.883333, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.884722, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.885417, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.886806, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.888194, - "weight": 6.738384936613652e-05 - }, - { - "days": 1.890278, - "weight": 0.0002804625081725682 - }, - { - "days": 1.891667, - "weight": 0.00011291347731622876 - }, - { - "days": 1.894444, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.895833, - "weight": 9.105925590018449e-05 - }, - { - "days": 1.897222, - "weight": 7.831096007415866e-05 - }, - { - "days": 1.897917, - "weight": 0.0002804625081725682 - }, - { - "days": 1.898611, - "weight": 9.105925590018449e-05 - }, - { - "days": 1.899306, - "weight": 0.0001930456225083911 - }, - { - "days": 1.9, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.900694, - "weight": 0.0001384100689682804 - }, - { - "days": 1.906944, - "weight": 0.00011109229219822507 - }, - { - "days": 1.907639, - "weight": 0.0010927110708022138 - }, - { - "days": 1.908333, - "weight": 5.645673865811438e-05 - }, - { - "days": 1.909722, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.916667, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.921528, - "weight": 3.096014700606273e-05 - }, - { - "days": 1.922917, - "weight": 7.831096007415866e-05 - }, - { - "days": 1.923611, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.925694, - "weight": 3.46025172420701e-05 - }, - { - "days": 1.932639, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.933333, - "weight": 0.00013476769873227304 - }, - { - "days": 1.936111, - "weight": 4.5529627950092245e-05 - }, - { - "days": 1.939583, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.940278, - "weight": 0.00012748295826025828 - }, - { - "days": 1.941667, - "weight": 3.8244887478077486e-05 - }, - { - "days": 1.942361, - "weight": 0.00010744992196221769 - }, - { - "days": 1.943056, - "weight": 6.009910889412176e-05 - }, - { - "days": 1.945139, - "weight": 6.009910889412176e-05 - }, - { - "days": 1.945833, - "weight": 0.00011291347731622876 - }, - { - "days": 1.946528, - "weight": 0.00017847614156436158 - }, - { - "days": 1.947222, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.947917, - "weight": 5.4635553540110695e-05 - }, - { - "days": 1.949306, - "weight": 7.648977495615497e-05 - }, - { - "days": 1.95, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.950694, - "weight": 0.00010198636660820662 - }, - { - "days": 1.951389, - "weight": 0.00011109229219822507 - }, - { - "days": 1.954861, - "weight": 0.00023493288022247597 - }, - { - "days": 1.955556, - "weight": 4.188725771408486e-05 - }, - { - "days": 1.956944, - "weight": 0.0001602642903843247 - }, - { - "days": 1.958333, - "weight": 6.92050344841402e-05 - }, - { - "days": 1.959028, - "weight": 0.0004625810199729372 - }, - { - "days": 1.959722, - "weight": 0.00018758206715438003 - }, - { - "days": 1.961111, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.963194, - "weight": 3.278133212406642e-05 - }, - { - "days": 1.963889, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.964583, - "weight": 0.0002312905099864686 - }, - { - "days": 1.965972, - "weight": 0.0004571174646189261 - }, - { - "days": 1.966667, - "weight": 5.4635553540110695e-05 - }, - { - "days": 1.968056, - "weight": 5.2814368422107004e-05 - }, - { - "days": 1.970139, - "weight": 6.192029401212545e-05 - }, - { - "days": 1.970833, - "weight": 5.2814368422107004e-05 - }, - { - "days": 1.972917, - "weight": 0.00010562873684421401 - }, - { - "days": 1.975694, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.98125, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.981944, - "weight": 8.195333031016604e-05 - }, - { - "days": 1.982639, - "weight": 0.00020215154809840956 - }, - { - "days": 1.9875, - "weight": 0.00010562873684421401 - }, - { - "days": 1.988194, - "weight": 0.00011109229219822507 - }, - { - "days": 1.990972, - "weight": 6.738384936613652e-05 - }, - { - "days": 1.99375, - "weight": 0.00012019821778824352 - }, - { - "days": 1.997222, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.997917, - "weight": 7.10262196021439e-05 - }, - { - "days": 1.998611, - "weight": 0.0002804625081725682 - }, - { - "days": 1.999306, - "weight": 9.470162613619187e-05 - }, - { - "days": 2.001389, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.002083, - "weight": 0.00012930414337826196 - }, - { - "days": 2.004167, - "weight": 0.0004261573176128634 - }, - { - "days": 2.004861, - "weight": 0.00021854221416044278 - }, - { - "days": 2.005556, - "weight": 3.8244887478077486e-05 - }, - { - "days": 2.006944, - "weight": 4.006607259608118e-05 - }, - { - "days": 2.008333, - "weight": 9.105925590018449e-05 - }, - { - "days": 2.009028, - "weight": 0.00021489984392443538 - }, - { - "days": 2.009722, - "weight": 0.00018576088203637635 - }, - { - "days": 2.010417, - "weight": 0.00015297954991230994 - }, - { - "days": 2.013194, - "weight": 4.5529627950092245e-05 - }, - { - "days": 2.014583, - "weight": 7.648977495615497e-05 - }, - { - "days": 2.015278, - "weight": 6.009910889412176e-05 - }, - { - "days": 2.015972, - "weight": 0.00029138961888059035 - }, - { - "days": 2.018056, - "weight": 6.556266424813284e-05 - }, - { - "days": 2.01875, - "weight": 0.00010927110708022139 - }, - { - "days": 2.020139, - "weight": 0.00013476769873227304 - }, - { - "days": 2.020833, - "weight": 0.00038062768966277115 - }, - { - "days": 2.023611, - "weight": 0.00013476769873227304 - }, - { - "days": 2.024306, - "weight": 6.738384936613652e-05 - }, - { - "days": 2.025694, - "weight": 0.0005700309419351549 - }, - { - "days": 2.03125, - "weight": 0.00034056161706669 - }, - { - "days": 2.031944, - "weight": 6.374147913012914e-05 - }, - { - "days": 2.034028, - "weight": 0.00010380755172621031 - }, - { - "days": 2.041667, - "weight": 7.10262196021439e-05 - }, - { - "days": 2.042361, - "weight": 0.00010380755172621031 - }, - { - "days": 2.044444, - "weight": 5.4635553540110695e-05 - }, - { - "days": 2.045139, - "weight": 5.645673865811438e-05 - }, - { - "days": 2.046528, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.048611, - "weight": 5.2814368422107004e-05 - }, - { - "days": 2.049306, - "weight": 8.195333031016604e-05 - }, - { - "days": 2.052778, - "weight": 9.652281125419556e-05 - }, - { - "days": 2.053472, - "weight": 5.4635553540110695e-05 - }, - { - "days": 2.054167, - "weight": 4.006607259608118e-05 - }, - { - "days": 2.056944, - "weight": 0.00011109229219822507 - }, - { - "days": 2.057639, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.059028, - "weight": 4.006607259608118e-05 - }, - { - "days": 2.059722, - "weight": 8.195333031016604e-05 - }, - { - "days": 2.063889, - "weight": 0.00012930414337826196 - }, - { - "days": 2.065278, - "weight": 6.738384936613652e-05 - }, - { - "days": 2.065972, - "weight": 4.006607259608118e-05 - }, - { - "days": 2.066667, - "weight": 7.831096007415866e-05 - }, - { - "days": 2.068056, - "weight": 0.00012019821778824352 - }, - { - "days": 2.070833, - "weight": 0.00018393969691837266 - }, - { - "days": 2.071528, - "weight": 7.10262196021439e-05 - }, - { - "days": 2.072917, - "weight": 8.74168856641771e-05 - }, - { - "days": 2.073611, - "weight": 5.4635553540110695e-05 - }, - { - "days": 2.075, - "weight": 0.00018393969691837266 - }, - { - "days": 2.077083, - "weight": 0.00013112532849626567 - }, - { - "days": 2.078472, - "weight": 0.00011109229219822507 - }, - { - "days": 2.079861, - "weight": 9.470162613619187e-05 - }, - { - "days": 2.080556, - "weight": 4.917199818609962e-05 - }, - { - "days": 2.082639, - "weight": 7.831096007415866e-05 - }, - { - "days": 2.083333, - "weight": 3.096014700606273e-05 - }, - { - "days": 2.086806, - "weight": 0.00040066072596081176 - }, - { - "days": 2.093056, - "weight": 0.0005645673865811438 - }, - { - "days": 2.095139, - "weight": 3.8244887478077486e-05 - }, - { - "days": 2.108333, - "weight": 8.74168856641771e-05 - }, - { - "days": 2.110417, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.115972, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.117361, - "weight": 3.096014700606273e-05 - }, - { - "days": 2.11875, - "weight": 0.0001930456225083911 - }, - { - "days": 2.120833, - "weight": 6.374147913012914e-05 - }, - { - "days": 2.122917, - "weight": 3.278133212406642e-05 - }, - { - "days": 2.125, - "weight": 4.917199818609962e-05 - }, - { - "days": 2.126389, - "weight": 3.8244887478077486e-05 - }, - { - "days": 2.127778, - "weight": 7.284740472014759e-05 - }, - { - "days": 2.129861, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.130556, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.132639, - "weight": 3.278133212406642e-05 - }, - { - "days": 2.134028, - "weight": 6.556266424813284e-05 - }, - { - "days": 2.136111, - "weight": 0.00011655584755223614 - }, - { - "days": 2.1375, - "weight": 0.00016754903085633945 - }, - { - "days": 2.138194, - "weight": 9.470162613619187e-05 - }, - { - "days": 2.138889, - "weight": 4.7350813068095936e-05 - }, - { - "days": 2.140278, - "weight": 0.00017301258621035053 - }, - { - "days": 2.140972, - "weight": 5.2814368422107004e-05 - }, - { - "days": 2.145139, - "weight": 4.7350813068095936e-05 - }, - { - "days": 2.15, - "weight": 0.00013658888385027673 - }, - { - "days": 2.152778, - "weight": 3.278133212406642e-05 - }, - { - "days": 2.154861, - "weight": 0.00020761510345242062 - }, - { - "days": 2.159028, - "weight": 0.00011291347731622876 - }, - { - "days": 2.164583, - "weight": 4.006607259608118e-05 - }, - { - "days": 2.165972, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.168056, - "weight": 3.46025172420701e-05 - }, - { - "days": 2.178472, - "weight": 3.096014700606273e-05 - }, - { - "days": 2.180556, - "weight": 6.556266424813284e-05 - }, - { - "days": 2.181944, - "weight": 0.00012019821778824352 - }, - { - "days": 2.182639, - "weight": 6.009910889412176e-05 - }, - { - "days": 2.184722, - "weight": 0.00012201940290624721 - }, - { - "days": 2.188194, - "weight": 3.8244887478077486e-05 - }, - { - "days": 2.189583, - "weight": 6.738384936613652e-05 - }, - { - "days": 2.190972, - "weight": 7.284740472014759e-05 - }, - { - "days": 2.191667, - "weight": 4.006607259608118e-05 - }, - { - "days": 2.195139, - "weight": 6.92050344841402e-05 - }, - { - "days": 2.196528, - "weight": 3.278133212406642e-05 - }, - { - "days": 2.197222, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.197917, - "weight": 4.006607259608118e-05 - }, - { - "days": 2.198611, - "weight": 7.466858983815127e-05 - }, - { - "days": 2.199306, - "weight": 6.192029401212545e-05 - }, - { - "days": 2.2, - "weight": 7.10262196021439e-05 - }, - { - "days": 2.204167, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.206944, - "weight": 0.00015480073503031363 - }, - { - "days": 2.210417, - "weight": 0.00028956843376258664 - }, - { - "days": 2.211806, - "weight": 3.096014700606273e-05 - }, - { - "days": 2.220833, - "weight": 0.00014751599455829886 - }, - { - "days": 2.238889, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.241667, - "weight": 5.2814368422107004e-05 - }, - { - "days": 2.24375, - "weight": 7.466858983815127e-05 - }, - { - "days": 2.245139, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.246528, - "weight": 5.645673865811438e-05 - }, - { - "days": 2.247917, - "weight": 0.00010198636660820662 - }, - { - "days": 2.250694, - "weight": 6.556266424813284e-05 - }, - { - "days": 2.252778, - "weight": 5.827792377611807e-05 - }, - { - "days": 2.253472, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.254167, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.25625, - "weight": 8.74168856641771e-05 - }, - { - "days": 2.256944, - "weight": 0.0001420524392042878 - }, - { - "days": 2.258333, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.259028, - "weight": 5.645673865811438e-05 - }, - { - "days": 2.261806, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.266667, - "weight": 0.00010744992196221769 - }, - { - "days": 2.269444, - "weight": 0.00024039643557648705 - }, - { - "days": 2.272222, - "weight": 0.00020033036298040588 - }, - { - "days": 2.299306, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.302778, - "weight": 6.192029401212545e-05 - }, - { - "days": 2.309722, - "weight": 4.7350813068095936e-05 - }, - { - "days": 2.311806, - "weight": 7.648977495615497e-05 - }, - { - "days": 2.3125, - "weight": 6.374147913012914e-05 - }, - { - "days": 2.313194, - "weight": 3.46025172420701e-05 - }, - { - "days": 2.314583, - "weight": 6.738384936613652e-05 - }, - { - "days": 2.315972, - "weight": 6.738384936613652e-05 - }, - { - "days": 2.318056, - "weight": 0.00013658888385027673 - }, - { - "days": 2.31875, - "weight": 5.827792377611807e-05 - }, - { - "days": 2.325694, - "weight": 0.00010380755172621031 - }, - { - "days": 2.330556, - "weight": 0.00027135658258254974 - }, - { - "days": 2.33125, - "weight": 0.00013476769873227304 - }, - { - "days": 2.336111, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.338194, - "weight": 0.00013112532849626567 - }, - { - "days": 2.360417, - "weight": 5.827792377611807e-05 - }, - { - "days": 2.36875, - "weight": 5.2814368422107004e-05 - }, - { - "days": 2.373611, - "weight": 0.00011291347731622876 - }, - { - "days": 2.375, - "weight": 3.46025172420701e-05 - }, - { - "days": 2.376389, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.379167, - "weight": 8.377451542816972e-05 - }, - { - "days": 2.379861, - "weight": 3.46025172420701e-05 - }, - { - "days": 2.384722, - "weight": 0.00018393969691837266 - }, - { - "days": 2.385417, - "weight": 5.2814368422107004e-05 - }, - { - "days": 2.386111, - "weight": 0.000287747248644583 - }, - { - "days": 2.392361, - "weight": 0.00015480073503031363 - }, - { - "days": 2.39375, - "weight": 3.278133212406642e-05 - }, - { - "days": 2.397222, - "weight": 8.559570054617342e-05 - }, - { - "days": 2.421528, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.43125, - "weight": 8.559570054617342e-05 - }, - { - "days": 2.432639, - "weight": 6.738384936613652e-05 - }, - { - "days": 2.434722, - "weight": 8.74168856641771e-05 - }, - { - "days": 2.435417, - "weight": 3.096014700606273e-05 - }, - { - "days": 2.436806, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.4375, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.440278, - "weight": 0.00019122443739038743 - }, - { - "days": 2.44375, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.447917, - "weight": 0.00013294651361426936 - }, - { - "days": 2.448611, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.450694, - "weight": 0.00029685317423460144 - }, - { - "days": 2.451389, - "weight": 6.192029401212545e-05 - }, - { - "days": 2.458333, - "weight": 0.00015115836479430626 - }, - { - "days": 2.478472, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.49375, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.495139, - "weight": 3.278133212406642e-05 - }, - { - "days": 2.495833, - "weight": 8.013214519216235e-05 - }, - { - "days": 2.496528, - "weight": 6.92050344841402e-05 - }, - { - "days": 2.499306, - "weight": 0.00029685317423460144 - }, - { - "days": 2.501389, - "weight": 4.5529627950092245e-05 - }, - { - "days": 2.505556, - "weight": 0.00039883954084280805 - }, - { - "days": 2.509722, - "weight": 7.10262196021439e-05 - }, - { - "days": 2.511111, - "weight": 5.4635553540110695e-05 - }, - { - "days": 2.5125, - "weight": 0.00015297954991230994 - }, - { - "days": 2.513194, - "weight": 4.917199818609962e-05 - }, - { - "days": 2.513889, - "weight": 0.00011655584755223614 - }, - { - "days": 2.532639, - "weight": 4.006607259608118e-05 - }, - { - "days": 2.538194, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.552083, - "weight": 3.278133212406642e-05 - }, - { - "days": 2.554167, - "weight": 5.645673865811438e-05 - }, - { - "days": 2.557639, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.561111, - "weight": 3.46025172420701e-05 - }, - { - "days": 2.5625, - "weight": 6.556266424813284e-05 - }, - { - "days": 2.565278, - "weight": 7.831096007415866e-05 - }, - { - "days": 2.565972, - "weight": 0.0005099318330410331 - }, - { - "days": 2.567361, - "weight": 0.00022946932486846491 - }, - { - "days": 2.568056, - "weight": 6.009910889412176e-05 - }, - { - "days": 2.570139, - "weight": 8.923807078218079e-05 - }, - { - "days": 2.570833, - "weight": 0.00021672102904243907 - }, - { - "days": 2.578472, - "weight": 0.0001620854755023284 - }, - { - "days": 2.602083, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.620139, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.621528, - "weight": 0.00019850917786240217 - }, - { - "days": 2.628472, - "weight": 0.00020215154809840956 - }, - { - "days": 2.629167, - "weight": 0.00010198636660820662 - }, - { - "days": 2.629861, - "weight": 0.00013294651361426936 - }, - { - "days": 2.63125, - "weight": 6.92050344841402e-05 - }, - { - "days": 2.632639, - "weight": 0.00020033036298040588 - }, - { - "days": 2.633333, - "weight": 0.0001766549564463579 - }, - { - "days": 2.636806, - "weight": 0.0006246664954752656 - }, - { - "days": 2.672917, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.674306, - "weight": 6.556266424813284e-05 - }, - { - "days": 2.680556, - "weight": 0.0003059590998246199 - }, - { - "days": 2.684722, - "weight": 0.0001238405880242509 - }, - { - "days": 2.686806, - "weight": 9.105925590018449e-05 - }, - { - "days": 2.688194, - "weight": 0.0005408919800470958 - }, - { - "days": 2.689583, - "weight": 6.374147913012914e-05 - }, - { - "days": 2.690278, - "weight": 0.00014751599455829886 - }, - { - "days": 2.69375, - "weight": 4.006607259608118e-05 - }, - { - "days": 2.694444, - "weight": 0.00010198636660820662 - }, - { - "days": 2.698611, - "weight": 0.0001438736243222915 - }, - { - "days": 2.700694, - "weight": 4.5529627950092245e-05 - }, - { - "days": 2.713889, - "weight": 3.096014700606273e-05 - }, - { - "days": 2.729861, - "weight": 6.192029401212545e-05 - }, - { - "days": 2.731944, - "weight": 3.096014700606273e-05 - }, - { - "days": 2.739583, - "weight": 0.00022582695463245752 - }, - { - "days": 2.747222, - "weight": 0.0007903943412136014 - }, - { - "days": 2.748611, - "weight": 8.013214519216235e-05 - }, - { - "days": 2.75, - "weight": 9.105925590018449e-05 - }, - { - "days": 2.750694, - "weight": 5.827792377611807e-05 - }, - { - "days": 2.751389, - "weight": 6.009910889412176e-05 - }, - { - "days": 2.754861, - "weight": 0.00012201940290624721 - }, - { - "days": 2.75625, - "weight": 0.00019850917786240217 - }, - { - "days": 2.757639, - "weight": 3.278133212406642e-05 - }, - { - "days": 2.780556, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.793056, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.794444, - "weight": 5.645673865811438e-05 - }, - { - "days": 2.804861, - "weight": 7.466858983815127e-05 - }, - { - "days": 2.805556, - "weight": 3.46025172420701e-05 - }, - { - "days": 2.80625, - "weight": 0.0014350938729869075 - }, - { - "days": 2.807639, - "weight": 9.652281125419556e-05 - }, - { - "days": 2.809028, - "weight": 3.278133212406642e-05 - }, - { - "days": 2.813889, - "weight": 0.0004134090217868376 - }, - { - "days": 2.816667, - "weight": 9.652281125419556e-05 - }, - { - "days": 2.822222, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.848611, - "weight": 6.374147913012914e-05 - }, - { - "days": 2.849306, - "weight": 6.374147913012914e-05 - }, - { - "days": 2.859028, - "weight": 3.46025172420701e-05 - }, - { - "days": 2.864583, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.867361, - "weight": 8.377451542816972e-05 - }, - { - "days": 2.868056, - "weight": 5.2814368422107004e-05 - }, - { - "days": 2.870833, - "weight": 8.559570054617342e-05 - }, - { - "days": 2.871528, - "weight": 0.0001948668076263948 - }, - { - "days": 2.873611, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.875, - "weight": 4.006607259608118e-05 - }, - { - "days": 2.875694, - "weight": 0.0003296345063586678 - }, - { - "days": 2.880556, - "weight": 0.006876795005581932 - }, - { - "days": 2.882639, - "weight": 0.0001602642903843247 - }, - { - "days": 2.890972, - "weight": 7.466858983815127e-05 - }, - { - "days": 2.895833, - "weight": 6.374147913012914e-05 - }, - { - "days": 2.913194, - "weight": 0.00011473466243423246 - }, - { - "days": 2.914583, - "weight": 0.0001766549564463579 - }, - { - "days": 2.91875, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.919444, - "weight": 5.645673865811438e-05 - }, - { - "days": 2.922917, - "weight": 0.00018940325227238374 - }, - { - "days": 2.925694, - "weight": 0.0003369192468306826 - }, - { - "days": 2.929167, - "weight": 0.00028956843376258664 - }, - { - "days": 2.934028, - "weight": 0.0002677142123465424 - }, - { - "days": 2.9375, - "weight": 7.284740472014759e-05 - }, - { - "days": 2.940972, - "weight": 0.00015115836479430626 - }, - { - "days": 2.941667, - "weight": 0.0009761552232499777 - }, - { - "days": 2.942361, - "weight": 0.004334420580848782 - }, - { - "days": 2.95, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.954167, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.957639, - "weight": 8.923807078218079e-05 - }, - { - "days": 2.959028, - "weight": 4.7350813068095936e-05 - }, - { - "days": 2.959722, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.960417, - "weight": 4.188725771408486e-05 - }, - { - "days": 2.965278, - "weight": 3.46025172420701e-05 - }, - { - "days": 2.965972, - "weight": 4.7350813068095936e-05 - }, - { - "days": 2.968056, - "weight": 0.0003879124301347859 - }, - { - "days": 2.969444, - "weight": 5.4635553540110695e-05 - }, - { - "days": 2.971528, - "weight": 3.8244887478077486e-05 - }, - { - "days": 2.972222, - "weight": 0.00046622339020894457 - }, - { - "days": 2.972917, - "weight": 4.7350813068095936e-05 - }, - { - "days": 2.974306, - "weight": 0.00012201940290624721 - }, - { - "days": 2.976389, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.977083, - "weight": 6.192029401212545e-05 - }, - { - "days": 2.978472, - "weight": 7.10262196021439e-05 - }, - { - "days": 2.982639, - "weight": 7.10262196021439e-05 - }, - { - "days": 2.984722, - "weight": 0.0002677142123465424 - }, - { - "days": 2.985417, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.986806, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.9875, - "weight": 0.0004971835372150073 - }, - { - "days": 2.990972, - "weight": 0.0008268180435736752 - }, - { - "days": 2.993056, - "weight": 0.0006246664954752656 - }, - { - "days": 2.995139, - "weight": 0.0005791368675251733 - }, - { - "days": 2.997917, - "weight": 0.0002458599909304981 - }, - { - "days": 3.0, - "weight": 0.0004680445753269483 - }, - { - "days": 3.008333, - "weight": 0.002460421094422985 - }, - { - "days": 3.011111, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.015278, - "weight": 8.923807078218079e-05 - }, - { - "days": 3.017361, - "weight": 6.192029401212545e-05 - }, - { - "days": 3.030556, - "weight": 7.10262196021439e-05 - }, - { - "days": 3.031944, - "weight": 5.2814368422107004e-05 - }, - { - "days": 3.032639, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.033333, - "weight": 0.00018393969691837266 - }, - { - "days": 3.036111, - "weight": 0.00014569480944029518 - }, - { - "days": 3.0375, - "weight": 5.827792377611807e-05 - }, - { - "days": 3.038194, - "weight": 0.0003223497658866531 - }, - { - "days": 3.042361, - "weight": 0.0006665537531893505 - }, - { - "days": 3.04375, - "weight": 8.923807078218079e-05 - }, - { - "days": 3.048611, - "weight": 0.0004971835372150073 - }, - { - "days": 3.049306, - "weight": 0.0001420524392042878 - }, - { - "days": 3.050694, - "weight": 0.0003478463575387047 - }, - { - "days": 3.052083, - "weight": 3.278133212406642e-05 - }, - { - "days": 3.055556, - "weight": 7.10262196021439e-05 - }, - { - "days": 3.056944, - "weight": 9.470162613619187e-05 - }, - { - "days": 3.059722, - "weight": 0.00041523020690484124 - }, - { - "days": 3.061111, - "weight": 0.00023493288022247597 - }, - { - "days": 3.090972, - "weight": 9.470162613619187e-05 - }, - { - "days": 3.095139, - "weight": 3.46025172420701e-05 - }, - { - "days": 3.098611, - "weight": 3.8244887478077486e-05 - }, - { - "days": 3.104167, - "weight": 5.2814368422107004e-05 - }, - { - "days": 3.105556, - "weight": 4.370844283208855e-05 - }, - { - "days": 3.106944, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.107639, - "weight": 0.0001930456225083911 - }, - { - "days": 3.109028, - "weight": 6.009910889412176e-05 - }, - { - "days": 3.1125, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.114583, - "weight": 0.00015297954991230994 - }, - { - "days": 3.115278, - "weight": 7.284740472014759e-05 - }, - { - "days": 3.115972, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.116667, - "weight": 7.648977495615497e-05 - }, - { - "days": 3.123611, - "weight": 7.648977495615497e-05 - }, - { - "days": 3.138194, - "weight": 3.46025172420701e-05 - }, - { - "days": 3.15, - "weight": 3.8244887478077486e-05 - }, - { - "days": 3.156944, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.157639, - "weight": 4.7350813068095936e-05 - }, - { - "days": 3.165278, - "weight": 8.377451542816972e-05 - }, - { - "days": 3.169444, - "weight": 5.645673865811438e-05 - }, - { - "days": 3.170139, - "weight": 4.7350813068095936e-05 - }, - { - "days": 3.174306, - "weight": 3.8244887478077486e-05 - }, - { - "days": 3.175, - "weight": 8.195333031016604e-05 - }, - { - "days": 3.177778, - "weight": 3.278133212406642e-05 - }, - { - "days": 3.18125, - "weight": 0.0001402312540862841 - }, - { - "days": 3.213194, - "weight": 0.00018758206715438003 - }, - { - "days": 3.224306, - "weight": 0.00011109229219822507 - }, - { - "days": 3.23125, - "weight": 6.192029401212545e-05 - }, - { - "days": 3.233333, - "weight": 3.8244887478077486e-05 - }, - { - "days": 3.234028, - "weight": 5.4635553540110695e-05 - }, - { - "days": 3.235417, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.236111, - "weight": 3.278133212406642e-05 - }, - { - "days": 3.275, - "weight": 5.827792377611807e-05 - }, - { - "days": 3.2875, - "weight": 0.00011109229219822507 - }, - { - "days": 3.291667, - "weight": 3.46025172420701e-05 - }, - { - "days": 3.293056, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.297917, - "weight": 3.278133212406642e-05 - }, - { - "days": 3.300694, - "weight": 0.00013294651361426936 - }, - { - "days": 3.343056, - "weight": 6.738384936613652e-05 - }, - { - "days": 3.347222, - "weight": 5.4635553540110695e-05 - }, - { - "days": 3.349306, - "weight": 5.827792377611807e-05 - }, - { - "days": 3.359028, - "weight": 3.8244887478077486e-05 - }, - { - "days": 3.404167, - "weight": 4.188725771408486e-05 - }, - { - "days": 3.406944, - "weight": 3.46025172420701e-05 - }, - { - "days": 3.409028, - "weight": 4.006607259608118e-05 - }, - { - "days": 3.417361, - "weight": 0.00010016518149020294 - }, - { - "days": 3.419444, - "weight": 5.2814368422107004e-05 - }, - { - "days": 3.478472, - "weight": 5.099318330410331e-05 - }, - { - "days": 3.479167, - "weight": 3.8244887478077486e-05 - }, - { - "days": 3.485417, - "weight": 4.917199818609962e-05 - }, - { - "days": 3.524306, - "weight": 3.46025172420701e-05 - }, - { - "days": 3.533333, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.544444, - "weight": 5.4635553540110695e-05 - }, - { - "days": 3.545139, - "weight": 9.652281125419556e-05 - }, - { - "days": 3.590278, - "weight": 4.5529627950092245e-05 - }, - { - "days": 3.611111, - "weight": 0.00010016518149020294 - }, - { - "days": 3.660417, - "weight": 4.7350813068095936e-05 - }, - { - "days": 3.670139, - "weight": 0.00018211851180036898 - }, - { - "days": 3.719444, - "weight": 6.009910889412176e-05 - }, - { - "days": 3.723611, - "weight": 3.46025172420701e-05 - }, - { - "days": 3.727778, - "weight": 0.00021489984392443538 - }, - { - "days": 3.757639, - "weight": 4.188725771408486e-05 - }, - { - "days": 3.767361, - "weight": 4.7350813068095936e-05 - }, - { - "days": 3.779167, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.782639, - "weight": 3.278133212406642e-05 - }, - { - "days": 3.784722, - "weight": 4.7350813068095936e-05 - }, - { - "days": 3.786111, - "weight": 0.00024221762069449073 - }, - { - "days": 3.798611, - "weight": 3.096014700606273e-05 - }, - { - "days": 3.804861, - "weight": 3.46025172420701e-05 - }, - { - "days": 3.809028, - "weight": 4.917199818609962e-05 - }, - { - "days": 3.822222, - "weight": 5.2814368422107004e-05 - }, - { - "days": 3.831944, - "weight": 4.5529627950092245e-05 - }, - { - "days": 3.839583, - "weight": 3.46025172420701e-05 - }, - { - "days": 3.840278, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.849306, - "weight": 0.00047168694556295565 - }, - { - "days": 3.857639, - "weight": 5.099318330410331e-05 - }, - { - "days": 3.86875, - "weight": 4.006607259608118e-05 - }, - { - "days": 3.884028, - "weight": 4.917199818609962e-05 - }, - { - "days": 3.89375, - "weight": 5.099318330410331e-05 - }, - { - "days": 3.898611, - "weight": 0.0009233408548278707 - }, - { - "days": 3.899306, - "weight": 3.46025172420701e-05 - }, - { - "days": 3.927083, - "weight": 9.105925590018449e-05 - }, - { - "days": 3.930556, - "weight": 6.92050344841402e-05 - }, - { - "days": 3.931944, - "weight": 0.0001948668076263948 - }, - { - "days": 3.932639, - "weight": 6.192029401212545e-05 - }, - { - "days": 3.943056, - "weight": 7.466858983815127e-05 - }, - { - "days": 3.94375, - "weight": 4.006607259608118e-05 - }, - { - "days": 3.945833, - "weight": 6.92050344841402e-05 - }, - { - "days": 3.952083, - "weight": 3.8244887478077486e-05 - }, - { - "days": 3.952778, - "weight": 0.0001238405880242509 - }, - { - "days": 3.953472, - "weight": 5.645673865811438e-05 - }, - { - "days": 3.95625, - "weight": 4.370844283208855e-05 - }, - { - "days": 3.959722, - "weight": 0.00013476769873227304 - }, - { - "days": 3.961111, - "weight": 4.370844283208855e-05 - }, - { - "days": 3.963889, - "weight": 8.923807078218079e-05 - }, - { - "days": 3.965278, - "weight": 0.00044072679855689293 - }, - { - "days": 3.968056, - "weight": 4.006607259608118e-05 - }, - { - "days": 3.984028, - "weight": 4.006607259608118e-05 - }, - { - "days": 3.99375, - "weight": 0.0001748337713283542 - }, - { - "days": 3.995833, - "weight": 9.288044101818817e-05 - }, - { - "days": 3.996528, - "weight": 6.738384936613652e-05 - }, - { - "days": 3.998611, - "weight": 8.923807078218079e-05 - }, - { - "days": 4.001389, - "weight": 0.00021672102904243907 - }, - { - "days": 4.002778, - "weight": 4.006607259608118e-05 - }, - { - "days": 4.004167, - "weight": 5.099318330410331e-05 - }, - { - "days": 4.00625, - "weight": 4.006607259608118e-05 - }, - { - "days": 4.007639, - "weight": 7.831096007415866e-05 - }, - { - "days": 4.008333, - "weight": 3.6423702360073794e-05 - }, - { - "days": 4.011806, - "weight": 0.00014569480944029518 - }, - { - "days": 4.015278, - "weight": 7.10262196021439e-05 - }, - { - "days": 4.018056, - "weight": 6.192029401212545e-05 - }, - { - "days": 4.01875, - "weight": 7.10262196021439e-05 - }, - { - "days": 4.019444, - "weight": 0.00024403880581249442 - }, - { - "days": 4.020833, - "weight": 0.0001256617731422546 - }, - { - "days": 4.021528, - "weight": 5.099318330410331e-05 - }, - { - "days": 4.025, - "weight": 0.0001948668076263948 - }, - { - "days": 4.029861, - "weight": 3.8244887478077486e-05 - }, - { - "days": 4.035417, - "weight": 4.188725771408486e-05 - }, - { - "days": 4.047917, - "weight": 6.009910889412176e-05 - }, - { - "days": 4.054167, - "weight": 4.006607259608118e-05 - }, - { - "days": 4.059722, - "weight": 5.827792377611807e-05 - }, - { - "days": 4.063194, - "weight": 9.105925590018449e-05 - }, - { - "days": 4.069444, - "weight": 3.096014700606273e-05 - }, - { - "days": 4.070139, - "weight": 7.466858983815127e-05 - }, - { - "days": 4.074306, - "weight": 9.105925590018449e-05 - }, - { - "days": 4.078472, - "weight": 3.46025172420701e-05 - }, - { - "days": 4.079861, - "weight": 4.370844283208855e-05 - }, - { - "days": 4.080556, - "weight": 3.46025172420701e-05 - }, - { - "days": 4.082639, - "weight": 0.00013658888385027673 - }, - { - "days": 4.095833, - "weight": 3.278133212406642e-05 - }, - { - "days": 4.11875, - "weight": 3.46025172420701e-05 - }, - { - "days": 4.127083, - "weight": 6.556266424813284e-05 - }, - { - "days": 4.129167, - "weight": 4.370844283208855e-05 - }, - { - "days": 4.131944, - "weight": 5.645673865811438e-05 - }, - { - "days": 4.1375, - "weight": 4.5529627950092245e-05 - }, - { - "days": 4.148611, - "weight": 9.470162613619187e-05 - }, - { - "days": 4.181944, - "weight": 5.2814368422107004e-05 - }, - { - "days": 4.209028, - "weight": 3.46025172420701e-05 - }, - { - "days": 4.211806, - "weight": 3.8244887478077486e-05 - }, - { - "days": 4.23125, - "weight": 5.4635553540110695e-05 - }, - { - "days": 4.268056, - "weight": 3.278133212406642e-05 - }, - { - "days": 4.273611, - "weight": 3.6423702360073794e-05 - }, - { - "days": 4.305556, - "weight": 3.278133212406642e-05 - }, - { - "days": 4.334722, - "weight": 0.00010744992196221769 - }, - { - "days": 4.438889, - "weight": 4.006607259608118e-05 - }, - { - "days": 4.450694, - "weight": 3.096014700606273e-05 - }, - { - "days": 4.467361, - "weight": 3.6423702360073794e-05 - }, - { - "days": 4.619444, - "weight": 5.4635553540110695e-05 - }, - { - "days": 4.731944, - "weight": 3.46025172420701e-05 - }, - { - "days": 4.760417, - "weight": 3.6423702360073794e-05 - }, - { - "days": 4.770833, - "weight": 0.00022400576951445383 - }, - { - "days": 4.801389, - "weight": 3.46025172420701e-05 - }, - { - "days": 4.827083, - "weight": 0.00017119140109234684 - }, - { - "days": 4.884722, - "weight": 0.00029138961888059035 - }, - { - "days": 4.886806, - "weight": 3.278133212406642e-05 - }, - { - "days": 4.900694, - "weight": 4.006607259608118e-05 - }, - { - "days": 4.940278, - "weight": 3.278133212406642e-05 - }, - { - "days": 4.942361, - "weight": 7.648977495615497e-05 - }, - { - "days": 4.95, - "weight": 0.00011109229219822507 - }, - { - "days": 4.951389, - "weight": 0.00029138961888059035 - }, - { - "days": 4.952083, - "weight": 3.278133212406642e-05 - }, - { - "days": 4.952778, - "weight": 3.278133212406642e-05 - }, - { - "days": 4.963889, - "weight": 4.7350813068095936e-05 - }, - { - "days": 4.970139, - "weight": 0.00018758206715438003 - }, - { - "days": 4.978472, - "weight": 4.006607259608118e-05 - }, - { - "days": 4.9875, - "weight": 7.466858983815127e-05 - }, - { - "days": 4.99375, - "weight": 3.6423702360073794e-05 - }, - { - "days": 5.0, - "weight": 3.096014700606273e-05 - }, - { - "days": 5.00625, - "weight": 3.8244887478077486e-05 - }, - { - "days": 5.007639, - "weight": 4.917199818609962e-05 - }, - { - "days": 5.011806, - "weight": 4.917199818609962e-05 - }, - { - "days": 5.016667, - "weight": 4.7350813068095936e-05 - }, - { - "days": 5.018056, - "weight": 0.00011837703267023983 - }, - { - "days": 5.019444, - "weight": 7.831096007415866e-05 - }, - { - "days": 5.027778, - "weight": 4.006607259608118e-05 - }, - { - "days": 5.03125, - "weight": 3.6423702360073794e-05 - }, - { - "days": 5.054861, - "weight": 8.195333031016604e-05 - }, - { - "days": 5.078472, - "weight": 5.645673865811438e-05 - }, - { - "days": 5.086111, - "weight": 4.006607259608118e-05 - }, - { - "days": 5.095833, - "weight": 4.370844283208855e-05 - }, - { - "days": 5.097917, - "weight": 3.278133212406642e-05 - }, - { - "days": 5.103472, - "weight": 7.10262196021439e-05 - }, - { - "days": 5.114583, - "weight": 3.8244887478077486e-05 - }, - { - "days": 5.13125, - "weight": 3.6423702360073794e-05 - }, - { - "days": 5.771528, - "weight": 7.10262196021439e-05 - }, - { - "days": 5.838194, - "weight": 0.00010562873684421401 - }, - { - "days": 5.891667, - "weight": 4.370844283208855e-05 - }, - { - "days": 5.91875, - "weight": 0.00016754903085633945 - }, - { - "days": 5.936111, - "weight": 5.4635553540110695e-05 - }, - { - "days": 5.947917, - "weight": 3.46025172420701e-05 - }, - { - "days": 5.986111, - "weight": 8.74168856641771e-05 - }, - { - "days": 5.997222, - "weight": 4.370844283208855e-05 - }, - { - "days": 5.998611, - "weight": 3.6423702360073794e-05 - }, - { - "days": 6.002778, - "weight": 5.4635553540110695e-05 - }, - { - "days": 6.00625, - "weight": 4.917199818609962e-05 - }, - { - "days": 6.017361, - "weight": 4.5529627950092245e-05 - }, - { - "days": 6.04375, - "weight": 6.374147913012914e-05 - }, - { - "days": 6.124306, - "weight": 4.370844283208855e-05 - }, - { - "days": 6.765972, - "weight": 4.188725771408486e-05 - }, - { - "days": 6.844444, - "weight": 7.648977495615497e-05 - }, - { - "days": 6.902083, - "weight": 7.831096007415866e-05 - }, - { - "days": 6.947917, - "weight": 3.6423702360073794e-05 - }, - { - "days": 6.963194, - "weight": 8.195333031016604e-05 - }, - { - "days": 6.970833, - "weight": 4.006607259608118e-05 - }, - { - "days": 7.010417, - "weight": 6.374147913012914e-05 - }, - { - "days": 7.017361, - "weight": 5.2814368422107004e-05 - }, - { - "days": 7.018056, - "weight": 3.278133212406642e-05 - }, - { - "days": 7.051389, - "weight": 3.8244887478077486e-05 - }, - { - "days": 7.965278, - "weight": 3.6423702360073794e-05 - }, - { - "days": 9.828472, - "weight": 0.00010016518149020294 - }, - { - "days": 9.863889, - "weight": 5.2814368422107004e-05 - }, - { - "days": 9.931944, - "weight": 0.00010380755172621031 - }, - { - "days": 9.985417, - "weight": 4.5529627950092245e-05 - }, - { - "days": 10.048611, - "weight": 4.188725771408486e-05 - }, - { - "days": 10.845833, - "weight": 4.006607259608118e-05 - }, - { - "days": 10.914583, - "weight": 3.46025172420701e-05 - }, - { - "days": 11.076389, - "weight": 7.10262196021439e-05 - }, - { - "days": 11.150694, - "weight": 3.46025172420701e-05 - }, - { - "days": 11.209722, - "weight": 5.4635553540110695e-05 - }, - { - "days": 11.26875, - "weight": 4.917199818609962e-05 - }, - { - "days": 11.844444, - "weight": 4.188725771408486e-05 - }, - { - "days": 11.904861, - "weight": 4.006607259608118e-05 - }, - { - "days": 12.906944, - "weight": 4.370844283208855e-05 - }, - { - "days": 16.880556, - "weight": 4.5529627950092245e-05 - }, - { - "days": 17.958333, - "weight": 5.4635553540110695e-05 - }, - { - "new_client": true, - "weight": 0.022094617851620764 - } - ] -} \ No newline at end of file diff --git a/tools/DeltaIndexTestTool/dist-tail-10.json b/tools/DeltaIndexTestTool/dist-tail-10.json deleted file mode 100644 index 6f560ff37c..0000000000 --- a/tools/DeltaIndexTestTool/dist-tail-10.json +++ /dev/null @@ -1,7301 +0,0 @@ -{ - "description": "Telemetry-derived from C:\\Users\\johnmcp\\Downloads\\export.csv: 610,103 download events, 1.99% net-new clients, observed ages 0-18.0 days, plus 10.0% reinstated stale tail out to 344 days", - "buckets": [ - { - "days": 0.0, - "weight": 4.5893864973692975e-05 - }, - { - "days": 0.000694, - "weight": 0.00048680278204238624 - }, - { - "days": 0.028472, - "weight": 0.0019603236610191716 - }, - { - "days": 0.042361, - "weight": 5.245013139850626e-05 - }, - { - "days": 0.043056, - "weight": 7.375799727914943e-05 - }, - { - "days": 0.047222, - "weight": 0.007354491862034299 - }, - { - "days": 0.049306, - "weight": 4.5893864973692975e-05 - }, - { - "days": 0.050694, - "weight": 0.0031929017488840686 - }, - { - "days": 0.051389, - "weight": 0.007974059039179155 - }, - { - "days": 0.052083, - "weight": 3.278133212406641e-05 - }, - { - "days": 0.054167, - "weight": 0.004607416230037535 - }, - { - "days": 0.054861, - "weight": 0.0072151712005070175 - }, - { - "days": 0.055556, - "weight": 0.005930142981243614 - }, - { - "days": 0.056944, - "weight": 0.006303850167457971 - }, - { - "days": 0.057639, - "weight": 0.008698526479121022 - }, - { - "days": 0.058333, - "weight": 0.00386000185760882 - }, - { - "days": 0.059028, - "weight": 0.039824401330922084 - }, - { - "days": 0.059722, - "weight": 0.018177248662794826 - }, - { - "days": 0.060417, - "weight": 0.007629855051876458 - }, - { - "days": 0.061111, - "weight": 0.01636280192972775 - }, - { - "days": 0.061806, - "weight": 0.018300178658260075 - }, - { - "days": 0.0625, - "weight": 0.003414175740721517 - }, - { - "days": 0.063194, - "weight": 0.012627369134190382 - }, - { - "days": 0.063889, - "weight": 0.014064830547830694 - }, - { - "days": 0.064583, - "weight": 0.0033289442771989443 - }, - { - "days": 0.065278, - "weight": 0.0074036638602204 - }, - { - "days": 0.065972, - "weight": 0.009255809125230151 - }, - { - "days": 0.066667, - "weight": 0.015251514770721898 - }, - { - "days": 0.067361, - "weight": 0.004218957444367347 - }, - { - "days": 0.069444, - "weight": 0.005913752315181581 - }, - { - "days": 0.070833, - "weight": 0.003612502800072119 - }, - { - "days": 0.072222, - "weight": 0.0038223033256661438 - }, - { - "days": 0.072917, - "weight": 0.003353530276291994 - }, - { - "days": 0.074306, - "weight": 0.011679988635804862 - }, - { - "days": 0.076389, - "weight": 5.9006397823319545e-05 - }, - { - "days": 0.078472, - "weight": 0.0047319852921089865 - }, - { - "days": 0.084028, - "weight": 0.0031404516174855625 - }, - { - "days": 0.084722, - "weight": 0.004556605165245232 - }, - { - "days": 0.085417, - "weight": 0.004640197562161601 - }, - { - "days": 0.0875, - "weight": 0.00021635679201883833 - }, - { - "days": 0.088889, - "weight": 0.002183236719462823 - }, - { - "days": 0.102083, - "weight": 0.0011735716900415777 - }, - { - "days": 0.109722, - "weight": 0.001124399691855478 - }, - { - "days": 0.110417, - "weight": 0.0018111685998546693 - }, - { - "days": 0.113194, - "weight": 0.004522184766514962 - }, - { - "days": 0.114583, - "weight": 0.0014522130130961422 - }, - { - "days": 0.115278, - "weight": 6.556266424813282e-05 - }, - { - "days": 0.115972, - "weight": 3.7698531942676374e-05 - }, - { - "days": 0.116667, - "weight": 0.001471881812370582 - }, - { - "days": 0.117361, - "weight": 0.0029355682917101473 - }, - { - "days": 0.118056, - "weight": 0.003019160688626517 - }, - { - "days": 0.11875, - "weight": 0.0019439329949571384 - }, - { - "days": 0.119444, - "weight": 0.0015308882101939016 - }, - { - "days": 0.120139, - "weight": 0.0019750752604750016 - }, - { - "days": 0.120833, - "weight": 0.00172265900311969 - }, - { - "days": 0.121528, - "weight": 0.00174724500221274 - }, - { - "days": 0.122222, - "weight": 0.001589894608017221 - }, - { - "days": 0.122917, - "weight": 0.001271915686413777 - }, - { - "days": 0.123611, - "weight": 0.0006097327775076353 - }, - { - "days": 0.124306, - "weight": 0.0018029732668236526 - }, - { - "days": 0.125, - "weight": 0.0006703782419371581 - }, - { - "days": 0.125694, - "weight": 0.004197649578486704 - }, - { - "days": 0.126389, - "weight": 0.0007736394381279673 - }, - { - "days": 0.127083, - "weight": 0.0025176063071283006 - }, - { - "days": 0.127778, - "weight": 0.00253727510640274 - }, - { - "days": 0.130556, - "weight": 0.00027864132305456454 - }, - { - "days": 0.13125, - "weight": 0.0013194486179936731 - }, - { - "days": 0.131944, - "weight": 0.0007047986406674278 - }, - { - "days": 0.132639, - "weight": 0.0013620643497549594 - }, - { - "days": 0.133333, - "weight": 0.002709377100054089 - }, - { - "days": 0.134028, - "weight": 0.0021045615223650635 - }, - { - "days": 0.134722, - "weight": 0.000649070376056515 - }, - { - "days": 0.136111, - "weight": 0.001339117417268113 - }, - { - "days": 0.1375, - "weight": 0.0005589217127153324 - }, - { - "days": 0.138194, - "weight": 0.0004933590484671995 - }, - { - "days": 0.14375, - "weight": 0.0003917369188825936 - }, - { - "days": 0.144444, - "weight": 0.0011555419573733411 - }, - { - "days": 0.147917, - "weight": 0.0010621151608197518 - }, - { - "days": 0.148611, - "weight": 0.0002393037245056848 - }, - { - "days": 0.149306, - "weight": 0.000899847566805623 - }, - { - "days": 0.150694, - "weight": 0.0012309390212586937 - }, - { - "days": 0.161111, - "weight": 0.0006949642410302079 - }, - { - "days": 0.164583, - "weight": 3.4420398730269734e-05 - }, - { - "days": 0.168056, - "weight": 0.000685129841392988 - }, - { - "days": 0.169444, - "weight": 0.001539083543224918 - }, - { - "days": 0.172222, - "weight": 0.000685129841392988 - }, - { - "days": 0.172917, - "weight": 6.064546442952286e-05 - }, - { - "days": 0.173611, - "weight": 0.0010178603624522622 - }, - { - "days": 0.175, - "weight": 0.0005179450475602494 - }, - { - "days": 0.175694, - "weight": 0.0012604422201703536 - }, - { - "days": 0.176389, - "weight": 0.0005851467784145854 - }, - { - "days": 0.177083, - "weight": 0.0010063868962088389 - }, - { - "days": 0.177778, - "weight": 0.001855423398222159 - }, - { - "days": 0.178472, - "weight": 0.0006048155776890253 - }, - { - "days": 0.179167, - "weight": 0.00219634925231245 - }, - { - "days": 0.179861, - "weight": 0.0010113040960274488 - }, - { - "days": 0.18125, - "weight": 0.0023930372450568483 - }, - { - "days": 0.181944, - "weight": 0.0013014188853254366 - }, - { - "days": 0.182639, - "weight": 0.0005261403805912659 - }, - { - "days": 0.183333, - "weight": 0.0005245013139850626 - }, - { - "days": 0.184028, - "weight": 0.0009949134299654155 - }, - { - "days": 0.184722, - "weight": 0.0013522299501177396 - }, - { - "days": 0.185417, - "weight": 0.0006916861078178013 - }, - { - "days": 0.186111, - "weight": 0.0024831859083980306 - }, - { - "days": 0.186806, - "weight": 0.0002983101223290044 - }, - { - "days": 0.1875, - "weight": 0.0004277963842190667 - }, - { - "days": 0.188889, - "weight": 0.0007867519709775939 - }, - { - "days": 0.190972, - "weight": 0.0004818855822237763 - }, - { - "days": 0.192361, - "weight": 0.001406319148122449 - }, - { - "days": 0.194444, - "weight": 0.0006359578432068885 - }, - { - "days": 0.195139, - "weight": 0.0027569100316339854 - }, - { - "days": 0.196528, - "weight": 0.0011375122247051046 - }, - { - "days": 0.197222, - "weight": 0.00039829318530740693 - }, - { - "days": 0.197917, - "weight": 0.0002589725237801247 - }, - { - "days": 0.2, - "weight": 0.00035567745354612057 - }, - { - "days": 0.202083, - "weight": 0.0014505739464899389 - }, - { - "days": 0.204167, - "weight": 0.000608093710901432 - }, - { - "days": 0.208333, - "weight": 0.0006523485092689216 - }, - { - "days": 0.210417, - "weight": 0.0007949473040086106 - }, - { - "days": 0.211111, - "weight": 0.00048680278204238624 - }, - { - "days": 0.2125, - "weight": 0.0005917030448393988 - }, - { - "days": 0.218056, - "weight": 0.00020324425916921175 - }, - { - "days": 0.222917, - "weight": 0.0003278133212406641 - }, - { - "days": 0.227083, - "weight": 0.00031470078839103757 - }, - { - "days": 0.229167, - "weight": 0.00010653932940321584 - }, - { - "days": 0.23125, - "weight": 0.0007211893067294611 - }, - { - "days": 0.232639, - "weight": 0.0001196518622528424 - }, - { - "days": 0.233333, - "weight": 9.834399637219924e-05 - }, - { - "days": 0.234722, - "weight": 0.00010817839600941917 - }, - { - "days": 0.235417, - "weight": 0.00010490026279701252 - }, - { - "days": 0.236111, - "weight": 0.0002770022564483612 - }, - { - "days": 0.2375, - "weight": 0.0002737241232359546 - }, - { - "days": 0.238194, - "weight": 0.00011309559582802913 - }, - { - "days": 0.238889, - "weight": 0.0004277963842190667 - }, - { - "days": 0.239583, - "weight": 0.00022783025826226156 - }, - { - "days": 0.240278, - "weight": 0.0004179619845818468 - }, - { - "days": 0.240972, - "weight": 0.0004441870502810999 - }, - { - "days": 0.242361, - "weight": 0.0003655118531833405 - }, - { - "days": 0.243056, - "weight": 0.0008113379700706437 - }, - { - "days": 0.24375, - "weight": 0.0005589217127153324 - }, - { - "days": 0.244444, - "weight": 0.00011473466243423244 - }, - { - "days": 0.245833, - "weight": 0.00024258185771809144 - }, - { - "days": 0.247917, - "weight": 0.00030650545536002095 - }, - { - "days": 0.248611, - "weight": 8.195333031016603e-05 - }, - { - "days": 0.249306, - "weight": 0.00025077719074910807 - }, - { - "days": 0.251389, - "weight": 0.00012784719528385901 - }, - { - "days": 0.252083, - "weight": 0.001186684222891204 - }, - { - "days": 0.252778, - "weight": 0.0002770022564483612 - }, - { - "days": 0.253472, - "weight": 0.0004687730493741497 - }, - { - "days": 0.254167, - "weight": 0.00023602559129327818 - }, - { - "days": 0.254861, - "weight": 0.0001114565292218258 - }, - { - "days": 0.255556, - "weight": 0.0001344034617086723 - }, - { - "days": 0.25625, - "weight": 0.00012129092885904572 - }, - { - "days": 0.258333, - "weight": 0.0002622506569925313 - }, - { - "days": 0.259028, - "weight": 8.850959673497932e-05 - }, - { - "days": 0.259722, - "weight": 7.867519709775939e-05 - }, - { - "days": 0.261111, - "weight": 0.0002540553239615147 - }, - { - "days": 0.265278, - "weight": 0.0001507941277707055 - }, - { - "days": 0.268056, - "weight": 0.00019996612595680512 - }, - { - "days": 0.271528, - "weight": 0.00044910425009970985 - }, - { - "days": 0.274306, - "weight": 0.00010653932940321584 - }, - { - "days": 0.275, - "weight": 0.00011473466243423244 - }, - { - "days": 0.276389, - "weight": 0.00012948626189006234 - }, - { - "days": 0.279861, - "weight": 0.00022783025826226156 - }, - { - "days": 0.281944, - "weight": 0.00016062852740792542 - }, - { - "days": 0.284028, - "weight": 4.2615731761286336e-05 - }, - { - "days": 0.286806, - "weight": 0.00013112532849626565 - }, - { - "days": 0.288194, - "weight": 8.850959673497932e-05 - }, - { - "days": 0.290278, - "weight": 0.0002393037245056848 - }, - { - "days": 0.292361, - "weight": 0.00010981746261562248 - }, - { - "days": 0.293056, - "weight": 0.0001425987947396889 - }, - { - "days": 0.295139, - "weight": 0.00013112532849626565 - }, - { - "days": 0.297222, - "weight": 7.867519709775939e-05 - }, - { - "days": 0.297917, - "weight": 0.00039501505209500027 - }, - { - "days": 0.299306, - "weight": 0.0004458261168873032 - }, - { - "days": 0.3, - "weight": 0.00024258185771809144 - }, - { - "days": 0.300694, - "weight": 0.0003343695876654774 - }, - { - "days": 0.301389, - "weight": 0.0002065223923816184 - }, - { - "days": 0.303472, - "weight": 7.375799727914943e-05 - }, - { - "days": 0.304167, - "weight": 0.00034584305390890065 - }, - { - "days": 0.305556, - "weight": 0.00035567745354612057 - }, - { - "days": 0.30625, - "weight": 0.00019832705935060179 - }, - { - "days": 0.306944, - "weight": 0.00015735039419551879 - }, - { - "days": 0.308333, - "weight": 0.0002720850566297512 - }, - { - "days": 0.309722, - "weight": 0.001145707557736121 - }, - { - "days": 0.310417, - "weight": 0.00012948626189006234 - }, - { - "days": 0.311111, - "weight": 0.0001819363932885686 - }, - { - "days": 0.311806, - "weight": 0.00043599171725008327 - }, - { - "days": 0.3125, - "weight": 0.0002753631898421579 - }, - { - "days": 0.313194, - "weight": 0.00040157131851981353 - }, - { - "days": 0.313889, - "weight": 0.00012456906207145238 - }, - { - "days": 0.314583, - "weight": 0.000168823860438942 - }, - { - "days": 0.315972, - "weight": 0.00012620812867765568 - }, - { - "days": 0.316667, - "weight": 9.834399637219924e-05 - }, - { - "days": 0.317361, - "weight": 0.00012129092885904572 - }, - { - "days": 0.31875, - "weight": 4.75329315798963e-05 - }, - { - "days": 0.320139, - "weight": 0.000337647720877884 - }, - { - "days": 0.320833, - "weight": 9.014866334118263e-05 - }, - { - "days": 0.322222, - "weight": 0.00018521452650097523 - }, - { - "days": 0.323611, - "weight": 4.917199818609962e-05 - }, - { - "days": 0.326389, - "weight": 0.00017046292704514534 - }, - { - "days": 0.327778, - "weight": 0.00013768159492107894 - }, - { - "days": 0.33125, - "weight": 0.00020160519256300844 - }, - { - "days": 0.332639, - "weight": 0.006790652949500357 - }, - { - "days": 0.335417, - "weight": 0.00018849265971338186 - }, - { - "days": 0.338194, - "weight": 9.50658631597926e-05 - }, - { - "days": 0.340278, - "weight": 0.00015571132758931546 - }, - { - "days": 0.340972, - "weight": 0.0003392867874840874 - }, - { - "days": 0.343056, - "weight": 0.00013112532849626565 - }, - { - "days": 0.34375, - "weight": 0.00016554572722653538 - }, - { - "days": 0.345139, - "weight": 3.1142265517863095e-05 - }, - { - "days": 0.345833, - "weight": 0.00015735039419551879 - }, - { - "days": 0.349306, - "weight": 0.00010653932940321584 - }, - { - "days": 0.353472, - "weight": 0.0001344034617086723 - }, - { - "days": 0.354861, - "weight": 0.00012292999546524905 - }, - { - "days": 0.356944, - "weight": 7.703613049155608e-05 - }, - { - "days": 0.357639, - "weight": 0.00015407226098311215 - }, - { - "days": 0.358333, - "weight": 6.720173085433615e-05 - }, - { - "days": 0.359028, - "weight": 0.000603176511082822 - }, - { - "days": 0.359722, - "weight": 0.0002474990575367014 - }, - { - "days": 0.360417, - "weight": 0.00019668799274439848 - }, - { - "days": 0.361111, - "weight": 0.00011473466243423244 - }, - { - "days": 0.361806, - "weight": 4.917199818609962e-05 - }, - { - "days": 0.3625, - "weight": 0.0003081445219662243 - }, - { - "days": 0.363194, - "weight": 0.00023110839147466822 - }, - { - "days": 0.363889, - "weight": 9.178772994738595e-05 - }, - { - "days": 0.365972, - "weight": 0.00019340985953199182 - }, - { - "days": 0.366667, - "weight": 9.342679655358928e-05 - }, - { - "days": 0.367361, - "weight": 6.884079746053947e-05 - }, - { - "days": 0.36875, - "weight": 0.00011473466243423244 - }, - { - "days": 0.369444, - "weight": 0.0001425987947396889 - }, - { - "days": 0.370139, - "weight": 0.0002720850566297512 - }, - { - "days": 0.371528, - "weight": 0.00042615731761286334 - }, - { - "days": 0.372222, - "weight": 0.00016226759401412875 - }, - { - "days": 0.372917, - "weight": 0.00022619119165605826 - }, - { - "days": 0.373611, - "weight": 0.00020160519256300844 - }, - { - "days": 0.375, - "weight": 0.0016489010058405407 - }, - { - "days": 0.376389, - "weight": 0.000388458785670187 - }, - { - "days": 0.377083, - "weight": 0.00015571132758931546 - }, - { - "days": 0.377778, - "weight": 0.0001835754598947719 - }, - { - "days": 0.378472, - "weight": 0.00016226759401412875 - }, - { - "days": 0.379861, - "weight": 0.00013112532849626565 - }, - { - "days": 0.38125, - "weight": 0.00013768159492107894 - }, - { - "days": 0.3875, - "weight": 0.00012948626189006234 - }, - { - "days": 0.388889, - "weight": 5.081106479230294e-05 - }, - { - "days": 0.390278, - "weight": 0.00011637372904043576 - }, - { - "days": 0.392361, - "weight": 0.00011637372904043576 - }, - { - "days": 0.397222, - "weight": 0.00021143959220022837 - }, - { - "days": 0.398611, - "weight": 0.0002393037245056848 - }, - { - "days": 0.399306, - "weight": 0.00013112532849626565 - }, - { - "days": 0.400694, - "weight": 0.000214717725412635 - }, - { - "days": 0.404167, - "weight": 0.00013276439510246898 - }, - { - "days": 0.406944, - "weight": 0.0018685359310717856 - }, - { - "days": 0.407639, - "weight": 0.00016390666062033205 - }, - { - "days": 0.409028, - "weight": 0.00012620812867765568 - }, - { - "days": 0.4125, - "weight": 0.00026552879020493795 - }, - { - "days": 0.414583, - "weight": 7.211893067294611e-05 - }, - { - "days": 0.415972, - "weight": 8.850959673497932e-05 - }, - { - "days": 0.418056, - "weight": 0.0002737241232359546 - }, - { - "days": 0.41875, - "weight": 0.00010817839600941917 - }, - { - "days": 0.419444, - "weight": 0.000214717725412635 - }, - { - "days": 0.420139, - "weight": 0.0003114226551786309 - }, - { - "days": 0.421528, - "weight": 0.00042615731761286334 - }, - { - "days": 0.422222, - "weight": 0.00021307865880643167 - }, - { - "days": 0.422917, - "weight": 8.687053012877599e-05 - }, - { - "days": 0.424306, - "weight": 0.00021963492523124496 - }, - { - "days": 0.425, - "weight": 0.0003097835885724276 - }, - { - "days": 0.426389, - "weight": 0.00012292999546524905 - }, - { - "days": 0.427083, - "weight": 0.000214717725412635 - }, - { - "days": 0.427778, - "weight": 0.0002737241232359546 - }, - { - "days": 0.429861, - "weight": 0.0015833383415924077 - }, - { - "days": 0.43125, - "weight": 4.917199818609962e-05 - }, - { - "days": 0.431944, - "weight": 0.00017865826007616194 - }, - { - "days": 0.432639, - "weight": 0.00021799585862504163 - }, - { - "days": 0.434028, - "weight": 0.00019177079292578852 - }, - { - "days": 0.435417, - "weight": 0.00013276439510246898 - }, - { - "days": 0.436806, - "weight": 4.425479836748966e-05 - }, - { - "days": 0.4375, - "weight": 0.0007441362392163075 - }, - { - "days": 0.438194, - "weight": 0.0002212739918374483 - }, - { - "days": 0.438889, - "weight": 0.0002720850566297512 - }, - { - "days": 0.440278, - "weight": 0.00014751599455829886 - }, - { - "days": 0.442361, - "weight": 0.000168823860438942 - }, - { - "days": 0.443056, - "weight": 0.00014587692795209553 - }, - { - "days": 0.446528, - "weight": 0.00011801279564663909 - }, - { - "days": 0.447222, - "weight": 0.0003360086542716807 - }, - { - "days": 0.447917, - "weight": 0.00020488332577541508 - }, - { - "days": 0.449306, - "weight": 0.00015407226098311215 - }, - { - "days": 0.453472, - "weight": 0.0003737071862143571 - }, - { - "days": 0.457639, - "weight": 0.00036059465336473053 - }, - { - "days": 0.458333, - "weight": 0.00043599171725008327 - }, - { - "days": 0.459028, - "weight": 7.867519709775939e-05 - }, - { - "days": 0.460417, - "weight": 0.00021963492523124496 - }, - { - "days": 0.4625, - "weight": 0.0009768836972971792 - }, - { - "days": 0.466667, - "weight": 0.0019849096601122214 - }, - { - "days": 0.468056, - "weight": 0.00042943545082527 - }, - { - "days": 0.470833, - "weight": 4.5893864973692975e-05 - }, - { - "days": 0.472222, - "weight": 0.00019340985953199182 - }, - { - "days": 0.472917, - "weight": 0.0001753801268637553 - }, - { - "days": 0.473611, - "weight": 0.0004818855822237763 - }, - { - "days": 0.474306, - "weight": 0.0005228622473788593 - }, - { - "days": 0.475, - "weight": 0.0002851975894793778 - }, - { - "days": 0.477083, - "weight": 0.0009473804983855193 - }, - { - "days": 0.477778, - "weight": 0.00031797892160344423 - }, - { - "days": 0.479167, - "weight": 0.00045074331670591315 - }, - { - "days": 0.480556, - "weight": 0.0004933590484671995 - }, - { - "days": 0.48125, - "weight": 0.0005425310466532991 - }, - { - "days": 0.483333, - "weight": 0.00042451825100666004 - }, - { - "days": 0.484028, - "weight": 0.0013161704847812665 - }, - { - "days": 0.484722, - "weight": 0.0002950319891165977 - }, - { - "days": 0.485417, - "weight": 0.0002851975894793778 - }, - { - "days": 0.486806, - "weight": 0.0011883232894974074 - }, - { - "days": 0.4875, - "weight": 0.0005343357136222825 - }, - { - "days": 0.488194, - "weight": 0.0003392867874840874 - }, - { - "days": 0.488889, - "weight": 0.0002983101223290044 - }, - { - "days": 0.490278, - "weight": 0.0007801957045527806 - }, - { - "days": 0.490972, - "weight": 0.00022783025826226156 - }, - { - "days": 0.491667, - "weight": 0.00039829318530740693 - }, - { - "days": 0.492361, - "weight": 0.0002540553239615147 - }, - { - "days": 0.493056, - "weight": 0.0007801957045527806 - }, - { - "days": 0.49375, - "weight": 0.0006310406433882784 - }, - { - "days": 0.494444, - "weight": 0.00346498680551382 - }, - { - "days": 0.495833, - "weight": 0.0003819025192453737 - }, - { - "days": 0.498611, - "weight": 0.0007375799727914943 - }, - { - "days": 0.499306, - "weight": 0.000260611590386328 - }, - { - "days": 0.500694, - "weight": 0.00029011478929798776 - }, - { - "days": 0.502083, - "weight": 0.00031797892160344423 - }, - { - "days": 0.50625, - "weight": 0.0003655118531833405 - }, - { - "days": 0.506944, - "weight": 0.0007490534390349175 - }, - { - "days": 0.509028, - "weight": 0.0003655118531833405 - }, - { - "days": 0.511111, - "weight": 0.0010916183597314115 - }, - { - "days": 0.5125, - "weight": 0.00013768159492107894 - }, - { - "days": 0.517361, - "weight": 0.0004458261168873032 - }, - { - "days": 0.51875, - "weight": 0.0002622506569925313 - }, - { - "days": 0.521528, - "weight": 0.0005408919800470958 - }, - { - "days": 0.522917, - "weight": 0.00026716785681114125 - }, - { - "days": 0.524306, - "weight": 0.00038681971906398365 - }, - { - "days": 0.525, - "weight": 0.00018029732668236527 - }, - { - "days": 0.526389, - "weight": 0.00025077719074910807 - }, - { - "days": 0.527083, - "weight": 0.0001819363932885686 - }, - { - "days": 0.532639, - "weight": 0.0004032103851260169 - }, - { - "days": 0.533333, - "weight": 0.0007244674399418677 - }, - { - "days": 0.535417, - "weight": 0.0004458261168873032 - }, - { - "days": 0.536111, - "weight": 0.00030486638875381765 - }, - { - "days": 0.536806, - "weight": 0.0013489518169053328 - }, - { - "days": 0.538194, - "weight": 8.523146352257267e-05 - }, - { - "days": 0.539583, - "weight": 0.000567117045746349 - }, - { - "days": 0.540278, - "weight": 0.0005408919800470958 - }, - { - "days": 0.543056, - "weight": 0.0004589386497369298 - }, - { - "days": 0.544444, - "weight": 0.0008490365020133201 - }, - { - "days": 0.545833, - "weight": 0.0003671509197895438 - }, - { - "days": 0.546528, - "weight": 0.0007064377072736312 - }, - { - "days": 0.547222, - "weight": 0.00031961798820964753 - }, - { - "days": 0.548611, - "weight": 0.00020160519256300844 - }, - { - "days": 0.549306, - "weight": 0.000301588255541411 - }, - { - "days": 0.55, - "weight": 0.001435822347034109 - }, - { - "days": 0.550694, - "weight": 0.0009539367648103326 - }, - { - "days": 0.551389, - "weight": 0.0004130447847632368 - }, - { - "days": 0.552083, - "weight": 9.670492976599591e-05 - }, - { - "days": 0.552778, - "weight": 0.00019504892613819515 - }, - { - "days": 0.553472, - "weight": 0.000506471581316826 - }, - { - "days": 0.554167, - "weight": 6.064546442952286e-05 - }, - { - "days": 0.554861, - "weight": 0.000260611590386328 - }, - { - "days": 0.555556, - "weight": 0.00023110839147466822 - }, - { - "days": 0.556944, - "weight": 0.00017374106025755198 - }, - { - "days": 0.557639, - "weight": 0.00029175385590419106 - }, - { - "days": 0.558333, - "weight": 0.0003228961214220542 - }, - { - "days": 0.559722, - "weight": 0.00039665411870120357 - }, - { - "days": 0.560417, - "weight": 0.0001032611961908092 - }, - { - "days": 0.568056, - "weight": 0.0001360425283148756 - }, - { - "days": 0.570139, - "weight": 0.0005277794471974693 - }, - { - "days": 0.570833, - "weight": 8.523146352257267e-05 - }, - { - "days": 0.572917, - "weight": 0.00014751599455829886 - }, - { - "days": 0.575, - "weight": 0.00016554572722653538 - }, - { - "days": 0.58125, - "weight": 0.00023274745808087152 - }, - { - "days": 0.584722, - "weight": 0.0006277625101758718 - }, - { - "days": 0.5875, - "weight": 3.605946533647306e-05 - }, - { - "days": 0.590278, - "weight": 7.211893067294611e-05 - }, - { - "days": 0.591667, - "weight": 0.0003638727865771372 - }, - { - "days": 0.592361, - "weight": 0.0002081614589878217 - }, - { - "days": 0.593056, - "weight": 8.359239691636936e-05 - }, - { - "days": 0.59375, - "weight": 0.00010817839600941917 - }, - { - "days": 0.595833, - "weight": 0.00011637372904043576 - }, - { - "days": 0.597222, - "weight": 0.0002458599909304981 - }, - { - "days": 0.597917, - "weight": 0.0010899792931252082 - }, - { - "days": 0.598611, - "weight": 0.00038518065245778034 - }, - { - "days": 0.599306, - "weight": 8.687053012877599e-05 - }, - { - "days": 0.6, - "weight": 0.00012456906207145238 - }, - { - "days": 0.601389, - "weight": 0.0002720850566297512 - }, - { - "days": 0.602083, - "weight": 5.57282646109129e-05 - }, - { - "days": 0.602778, - "weight": 0.00019668799274439848 - }, - { - "days": 0.603472, - "weight": 0.00020488332577541508 - }, - { - "days": 0.605556, - "weight": 0.00043599171725008327 - }, - { - "days": 0.606944, - "weight": 0.00016390666062033205 - }, - { - "days": 0.607639, - "weight": 0.0001425987947396889 - }, - { - "days": 0.609028, - "weight": 0.0001491550611645022 - }, - { - "days": 0.609722, - "weight": 0.00023438652468707485 - }, - { - "days": 0.610417, - "weight": 0.00012456906207145238 - }, - { - "days": 0.611111, - "weight": 0.0017046292704514534 - }, - { - "days": 0.611806, - "weight": 0.0002983101223290044 - }, - { - "days": 0.6125, - "weight": 6.392359764192951e-05 - }, - { - "days": 0.613194, - "weight": 4.917199818609962e-05 - }, - { - "days": 0.613889, - "weight": 0.0002835585228731745 - }, - { - "days": 0.614583, - "weight": 0.00025733345717392133 - }, - { - "days": 0.615278, - "weight": 7.211893067294611e-05 - }, - { - "days": 0.615972, - "weight": 7.211893067294611e-05 - }, - { - "days": 0.616667, - "weight": 0.0003360086542716807 - }, - { - "days": 0.617361, - "weight": 0.0002065223923816184 - }, - { - "days": 0.61875, - "weight": 0.00017865826007616194 - }, - { - "days": 0.619444, - "weight": 8.195333031016603e-05 - }, - { - "days": 0.620139, - "weight": 0.000296671055722801 - }, - { - "days": 0.622222, - "weight": 7.703613049155608e-05 - }, - { - "days": 0.623611, - "weight": 9.342679655358928e-05 - }, - { - "days": 0.624306, - "weight": 0.00010817839600941917 - }, - { - "days": 0.628472, - "weight": 7.703613049155608e-05 - }, - { - "days": 0.631944, - "weight": 0.0002458599909304981 - }, - { - "days": 0.632639, - "weight": 7.867519709775939e-05 - }, - { - "days": 0.634028, - "weight": 0.00040976665155083015 - }, - { - "days": 0.642361, - "weight": 0.0001032611961908092 - }, - { - "days": 0.644444, - "weight": 0.0004982762482858095 - }, - { - "days": 0.648611, - "weight": 0.00010653932940321584 - }, - { - "days": 0.652778, - "weight": 0.0015046631444946483 - }, - { - "days": 0.654167, - "weight": 0.0004212401177942534 - }, - { - "days": 0.654861, - "weight": 0.00012784719528385901 - }, - { - "days": 0.655556, - "weight": 0.0003638727865771372 - }, - { - "days": 0.65625, - "weight": 5.408919800470958e-05 - }, - { - "days": 0.656944, - "weight": 7.375799727914943e-05 - }, - { - "days": 0.657639, - "weight": 4.75329315798963e-05 - }, - { - "days": 0.658333, - "weight": 0.00019504892613819515 - }, - { - "days": 0.659028, - "weight": 6.720173085433615e-05 - }, - { - "days": 0.660417, - "weight": 0.00021635679201883833 - }, - { - "days": 0.661806, - "weight": 0.00016226759401412875 - }, - { - "days": 0.6625, - "weight": 6.884079746053947e-05 - }, - { - "days": 0.663194, - "weight": 9.998306297840256e-05 - }, - { - "days": 0.663889, - "weight": 5.408919800470958e-05 - }, - { - "days": 0.664583, - "weight": 0.0008031426370396271 - }, - { - "days": 0.665278, - "weight": 0.00011637372904043576 - }, - { - "days": 0.665972, - "weight": 0.00016226759401412875 - }, - { - "days": 0.666667, - "weight": 0.00010490026279701252 - }, - { - "days": 0.667361, - "weight": 0.0001425987947396889 - }, - { - "days": 0.668056, - "weight": 3.7698531942676374e-05 - }, - { - "days": 0.66875, - "weight": 0.0009424632985669094 - }, - { - "days": 0.669444, - "weight": 0.00019504892613819515 - }, - { - "days": 0.670139, - "weight": 0.0015243319437690881 - }, - { - "days": 0.670833, - "weight": 0.0001425987947396889 - }, - { - "days": 0.671528, - "weight": 0.00012784719528385901 - }, - { - "days": 0.672917, - "weight": 0.0002229130584436516 - }, - { - "days": 0.673611, - "weight": 0.00019340985953199182 - }, - { - "days": 0.674306, - "weight": 0.00015243319437690882 - }, - { - "days": 0.675, - "weight": 7.047986406674279e-05 - }, - { - "days": 0.675694, - "weight": 0.00022619119165605826 - }, - { - "days": 0.676389, - "weight": 0.0001753801268637553 - }, - { - "days": 0.677083, - "weight": 0.00020980052559402504 - }, - { - "days": 0.678472, - "weight": 9.014866334118263e-05 - }, - { - "days": 0.68125, - "weight": 5.408919800470958e-05 - }, - { - "days": 0.681944, - "weight": 0.0002540553239615147 - }, - { - "days": 0.682639, - "weight": 0.00011473466243423244 - }, - { - "days": 0.683333, - "weight": 0.0003523993203337139 - }, - { - "days": 0.684028, - "weight": 0.0004064885183384235 - }, - { - "days": 0.6875, - "weight": 0.00012129092885904572 - }, - { - "days": 0.688889, - "weight": 0.00016226759401412875 - }, - { - "days": 0.690278, - "weight": 5.081106479230294e-05 - }, - { - "days": 0.69375, - "weight": 9.342679655358928e-05 - }, - { - "days": 0.695833, - "weight": 6.228453103572619e-05 - }, - { - "days": 0.698611, - "weight": 0.0008146161032830504 - }, - { - "days": 0.70625, - "weight": 2.9503198911659772e-05 - }, - { - "days": 0.707639, - "weight": 0.0001032611961908092 - }, - { - "days": 0.713889, - "weight": 0.00039337598548879697 - }, - { - "days": 0.716667, - "weight": 0.00014587692795209553 - }, - { - "days": 0.71875, - "weight": 0.0007621659718845441 - }, - { - "days": 0.719444, - "weight": 0.0007015205074550212 - }, - { - "days": 0.720139, - "weight": 0.0013833722156356026 - }, - { - "days": 0.720833, - "weight": 0.0003523993203337139 - }, - { - "days": 0.721528, - "weight": 0.00018521452650097523 - }, - { - "days": 0.722222, - "weight": 8.850959673497932e-05 - }, - { - "days": 0.723611, - "weight": 0.00019340985953199182 - }, - { - "days": 0.725, - "weight": 8.195333031016603e-05 - }, - { - "days": 0.725694, - "weight": 0.00010981746261562248 - }, - { - "days": 0.726389, - "weight": 0.0003409258540902907 - }, - { - "days": 0.727083, - "weight": 0.0001032611961908092 - }, - { - "days": 0.727778, - "weight": 0.00014095972813348557 - }, - { - "days": 0.728472, - "weight": 0.00021635679201883833 - }, - { - "days": 0.729167, - "weight": 0.00028028038966076784 - }, - { - "days": 0.729861, - "weight": 0.00010162212958460587 - }, - { - "days": 0.730556, - "weight": 0.0012374952876835071 - }, - { - "days": 0.73125, - "weight": 9.014866334118263e-05 - }, - { - "days": 0.731944, - "weight": 6.884079746053947e-05 - }, - { - "days": 0.732639, - "weight": 0.00012620812867765568 - }, - { - "days": 0.733333, - "weight": 7.375799727914943e-05 - }, - { - "days": 0.734028, - "weight": 0.00023602559129327818 - }, - { - "days": 0.734722, - "weight": 8.359239691636936e-05 - }, - { - "days": 0.735417, - "weight": 0.00032125705481585084 - }, - { - "days": 0.736806, - "weight": 9.50658631597926e-05 - }, - { - "days": 0.7375, - "weight": 0.00013768159492107894 - }, - { - "days": 0.738194, - "weight": 0.000644153176237905 - }, - { - "days": 0.738889, - "weight": 0.00024913812414290476 - }, - { - "days": 0.740278, - "weight": 0.00013276439510246898 - }, - { - "days": 0.740972, - "weight": 5.736733121711622e-05 - }, - { - "days": 0.741667, - "weight": 0.00010162212958460587 - }, - { - "days": 0.743056, - "weight": 3.9337598548879697e-05 - }, - { - "days": 0.744444, - "weight": 9.670492976599591e-05 - }, - { - "days": 0.746528, - "weight": 9.342679655358928e-05 - }, - { - "days": 0.747222, - "weight": 0.000506471581316826 - }, - { - "days": 0.748611, - "weight": 0.00012292999546524905 - }, - { - "days": 0.749306, - "weight": 0.0001360425283148756 - }, - { - "days": 0.75, - "weight": 5.736733121711622e-05 - }, - { - "days": 0.750694, - "weight": 0.00010981746261562248 - }, - { - "days": 0.7625, - "weight": 6.556266424813282e-05 - }, - { - "days": 0.776389, - "weight": 0.000214717725412635 - }, - { - "days": 0.777083, - "weight": 5.9006397823319545e-05 - }, - { - "days": 0.777778, - "weight": 5.408919800470958e-05 - }, - { - "days": 0.778472, - "weight": 0.0004769683824051663 - }, - { - "days": 0.779167, - "weight": 0.0001819363932885686 - }, - { - "days": 0.779861, - "weight": 0.002270107249591599 - }, - { - "days": 0.780556, - "weight": 0.00042287918440045674 - }, - { - "days": 0.78125, - "weight": 0.0001032611961908092 - }, - { - "days": 0.781944, - "weight": 9.178772994738595e-05 - }, - { - "days": 0.782639, - "weight": 0.00031306172178483427 - }, - { - "days": 0.783333, - "weight": 8.850959673497932e-05 - }, - { - "days": 0.784028, - "weight": 0.00019340985953199182 - }, - { - "days": 0.785417, - "weight": 0.00042615731761286334 - }, - { - "days": 0.786806, - "weight": 0.0002851975894793778 - }, - { - "days": 0.7875, - "weight": 0.0026093940370756864 - }, - { - "days": 0.788194, - "weight": 0.0005556435795029257 - }, - { - "days": 0.788889, - "weight": 4.2615731761286336e-05 - }, - { - "days": 0.789583, - "weight": 0.000735940906185291 - }, - { - "days": 0.790972, - "weight": 6.884079746053947e-05 - }, - { - "days": 0.791667, - "weight": 0.0003278133212406641 - }, - { - "days": 0.792361, - "weight": 0.00028847572269178446 - }, - { - "days": 0.793056, - "weight": 9.178772994738595e-05 - }, - { - "days": 0.79375, - "weight": 0.00012784719528385901 - }, - { - "days": 0.795833, - "weight": 0.00041140571815703346 - }, - { - "days": 0.796528, - "weight": 0.00014587692795209553 - }, - { - "days": 0.797917, - "weight": 0.00017210199365134867 - }, - { - "days": 0.798611, - "weight": 0.00035567745354612057 - }, - { - "days": 0.799306, - "weight": 9.834399637219924e-05 - }, - { - "days": 0.8, - "weight": 0.0001835754598947719 - }, - { - "days": 0.800694, - "weight": 6.228453103572619e-05 - }, - { - "days": 0.801389, - "weight": 0.00037042905300195046 - }, - { - "days": 0.802778, - "weight": 8.687053012877599e-05 - }, - { - "days": 0.804167, - "weight": 0.00010981746261562248 - }, - { - "days": 0.804861, - "weight": 0.0003360086542716807 - }, - { - "days": 0.805556, - "weight": 8.195333031016603e-05 - }, - { - "days": 0.80625, - "weight": 5.9006397823319545e-05 - }, - { - "days": 0.808333, - "weight": 9.178772994738595e-05 - }, - { - "days": 0.809028, - "weight": 9.342679655358928e-05 - }, - { - "days": 0.810417, - "weight": 0.00035567745354612057 - }, - { - "days": 0.811111, - "weight": 0.0002720850566297512 - }, - { - "days": 0.827083, - "weight": 9.998306297840256e-05 - }, - { - "days": 0.832639, - "weight": 0.00017046292704514534 - }, - { - "days": 0.836806, - "weight": 7.539706388535275e-05 - }, - { - "days": 0.838194, - "weight": 0.0003409258540902907 - }, - { - "days": 0.838889, - "weight": 0.0008785397009249799 - }, - { - "days": 0.839583, - "weight": 0.0004786074490113696 - }, - { - "days": 0.840278, - "weight": 9.178772994738595e-05 - }, - { - "days": 0.840972, - "weight": 5.245013139850626e-05 - }, - { - "days": 0.841667, - "weight": 0.00022455212504985493 - }, - { - "days": 0.842361, - "weight": 0.00010490026279701252 - }, - { - "days": 0.84375, - "weight": 0.000337647720877884 - }, - { - "days": 0.844444, - "weight": 0.0001491550611645022 - }, - { - "days": 0.845833, - "weight": 0.0002589725237801247 - }, - { - "days": 0.846528, - "weight": 0.0007031595740612245 - }, - { - "days": 0.847222, - "weight": 0.0009342679655358928 - }, - { - "days": 0.849306, - "weight": 0.0028077210964262884 - }, - { - "days": 0.85, - "weight": 0.00017210199365134867 - }, - { - "days": 0.850694, - "weight": 0.00048680278204238624 - }, - { - "days": 0.851389, - "weight": 0.00022783025826226156 - }, - { - "days": 0.852778, - "weight": 7.539706388535275e-05 - }, - { - "days": 0.854167, - "weight": 8.687053012877599e-05 - }, - { - "days": 0.854861, - "weight": 0.0005474482464719091 - }, - { - "days": 0.855556, - "weight": 0.00023766465789948148 - }, - { - "days": 0.856944, - "weight": 0.00028847572269178446 - }, - { - "days": 0.857639, - "weight": 0.00010817839600941917 - }, - { - "days": 0.858333, - "weight": 9.834399637219924e-05 - }, - { - "days": 0.860417, - "weight": 0.00023602559129327818 - }, - { - "days": 0.861111, - "weight": 0.0004671339827679464 - }, - { - "days": 0.861806, - "weight": 0.00013932066152728227 - }, - { - "days": 0.863889, - "weight": 0.00038681971906398365 - }, - { - "days": 0.864583, - "weight": 0.00018849265971338186 - }, - { - "days": 0.865278, - "weight": 0.00030486638875381765 - }, - { - "days": 0.865972, - "weight": 0.00017701919346995863 - }, - { - "days": 0.866667, - "weight": 0.00012784719528385901 - }, - { - "days": 0.868056, - "weight": 0.00016554572722653538 - }, - { - "days": 0.86875, - "weight": 0.00030322732214761435 - }, - { - "days": 0.870139, - "weight": 0.0002720850566297512 - }, - { - "days": 0.870833, - "weight": 0.0002442209243242948 - }, - { - "days": 0.872222, - "weight": 0.00023438652468707485 - }, - { - "days": 0.882639, - "weight": 0.00010981746261562248 - }, - { - "days": 0.890278, - "weight": 5.736733121711622e-05 - }, - { - "days": 0.891667, - "weight": 0.0003261742546344608 - }, - { - "days": 0.896528, - "weight": 0.00047532931579896296 - }, - { - "days": 0.897917, - "weight": 0.00010817839600941917 - }, - { - "days": 0.898611, - "weight": 0.00042943545082527 - }, - { - "days": 0.901389, - "weight": 0.00024913812414290476 - }, - { - "days": 0.902083, - "weight": 0.0001114565292218258 - }, - { - "days": 0.902778, - "weight": 0.0002294693248684649 - }, - { - "days": 0.903472, - "weight": 0.00021799585862504163 - }, - { - "days": 0.904861, - "weight": 0.000337647720877884 - }, - { - "days": 0.905556, - "weight": 0.0036420059989837783 - }, - { - "days": 0.90625, - "weight": 0.0005736733121711622 - }, - { - "days": 0.906944, - "weight": 0.0005917030448393988 - }, - { - "days": 0.907639, - "weight": 0.00041140571815703346 - }, - { - "days": 0.908333, - "weight": 0.00046385584955553974 - }, - { - "days": 0.909722, - "weight": 7.539706388535275e-05 - }, - { - "days": 0.913194, - "weight": 0.00018685359310717856 - }, - { - "days": 0.913889, - "weight": 0.00022455212504985493 - }, - { - "days": 0.914583, - "weight": 0.00039993225191361023 - }, - { - "days": 0.915278, - "weight": 0.00011309559582802913 - }, - { - "days": 0.915972, - "weight": 0.0008293677027388802 - }, - { - "days": 0.916667, - "weight": 0.00040157131851981353 - }, - { - "days": 0.917361, - "weight": 3.4420398730269734e-05 - }, - { - "days": 0.918056, - "weight": 0.00020488332577541508 - }, - { - "days": 0.921528, - "weight": 0.010753916003299988 - }, - { - "days": 0.922222, - "weight": 0.000214717725412635 - }, - { - "days": 0.922917, - "weight": 0.0008670662346815566 - }, - { - "days": 0.923611, - "weight": 0.0002999491889352077 - }, - { - "days": 0.924306, - "weight": 0.002501215641066267 - }, - { - "days": 0.925, - "weight": 0.00019668799274439848 - }, - { - "days": 0.925694, - "weight": 0.00046385584955553974 - }, - { - "days": 0.926389, - "weight": 9.998306297840256e-05 - }, - { - "days": 0.927083, - "weight": 0.00019177079292578852 - }, - { - "days": 0.930556, - "weight": 0.0005408919800470958 - }, - { - "days": 0.93125, - "weight": 0.00031961798820964753 - }, - { - "days": 0.931944, - "weight": 0.00017701919346995863 - }, - { - "days": 0.932639, - "weight": 0.00027864132305456454 - }, - { - "days": 0.933333, - "weight": 0.00031961798820964753 - }, - { - "days": 0.94375, - "weight": 0.00042615731761286334 - }, - { - "days": 0.948611, - "weight": 0.00021799585862504163 - }, - { - "days": 0.950694, - "weight": 0.0002950319891165977 - }, - { - "days": 0.951389, - "weight": 0.0011702935568291709 - }, - { - "days": 0.955556, - "weight": 0.0007457753058225109 - }, - { - "days": 0.95625, - "weight": 0.002658566035261786 - }, - { - "days": 0.956944, - "weight": 0.0009145991662614529 - }, - { - "days": 0.957639, - "weight": 0.00031306172178483427 - }, - { - "days": 0.958333, - "weight": 0.00021307865880643167 - }, - { - "days": 0.959028, - "weight": 0.0005408919800470958 - }, - { - "days": 0.959722, - "weight": 6.228453103572619e-05 - }, - { - "days": 0.960417, - "weight": 0.003273216012588031 - }, - { - "days": 0.961111, - "weight": 0.0016374275395971173 - }, - { - "days": 0.961806, - "weight": 0.000214717725412635 - }, - { - "days": 0.9625, - "weight": 0.001257164086957947 - }, - { - "days": 0.964583, - "weight": 0.0010211384956646688 - }, - { - "days": 0.965278, - "weight": 0.00047205118258655636 - }, - { - "days": 0.965972, - "weight": 0.0014276270140030922 - }, - { - "days": 0.968056, - "weight": 0.0008211723697078636 - }, - { - "days": 0.96875, - "weight": 0.00644644896219766 - }, - { - "days": 0.970139, - "weight": 0.00044746518349350655 - }, - { - "days": 0.972222, - "weight": 0.0016243150067474907 - }, - { - "days": 0.972917, - "weight": 0.007910135441537226 - }, - { - "days": 0.973611, - "weight": 0.0014030410149100424 - }, - { - "days": 0.974306, - "weight": 0.001457130212914752 - }, - { - "days": 0.975, - "weight": 0.0006998814408488179 - }, - { - "days": 0.975694, - "weight": 0.0002294693248684649 - }, - { - "days": 0.976389, - "weight": 0.0008621490348629466 - }, - { - "days": 0.977083, - "weight": 0.003612502800072119 - }, - { - "days": 0.977778, - "weight": 4.2615731761286336e-05 - }, - { - "days": 0.979167, - "weight": 0.0015472788762559346 - }, - { - "days": 0.979861, - "weight": 0.0007900301041900005 - }, - { - "days": 0.980556, - "weight": 0.0011768498232539843 - }, - { - "days": 0.98125, - "weight": 0.0015751430085613912 - }, - { - "days": 0.981944, - "weight": 0.003568248001704629 - }, - { - "days": 0.982639, - "weight": 0.0046992039599849204 - }, - { - "days": 0.983333, - "weight": 0.0010948964929438183 - }, - { - "days": 0.984722, - "weight": 0.0003753462528205604 - }, - { - "days": 0.985417, - "weight": 0.0008605099682567433 - }, - { - "days": 0.986111, - "weight": 0.007726559981642453 - }, - { - "days": 0.986806, - "weight": 0.000296671055722801 - }, - { - "days": 0.988194, - "weight": 0.009711469641754674 - }, - { - "days": 0.988889, - "weight": 0.0010194994290584655 - }, - { - "days": 0.989583, - "weight": 0.00038518065245778034 - }, - { - "days": 0.990278, - "weight": 0.0016898776709956236 - }, - { - "days": 0.990972, - "weight": 0.003122421884817326 - }, - { - "days": 0.991667, - "weight": 3.7698531942676374e-05 - }, - { - "days": 0.995139, - "weight": 0.0010358900951204987 - }, - { - "days": 0.995833, - "weight": 0.00013276439510246898 - }, - { - "days": 0.998611, - "weight": 0.0005113887811354361 - }, - { - "days": 1.002778, - "weight": 0.00038518065245778034 - }, - { - "days": 1.007639, - "weight": 0.0013899284820604158 - }, - { - "days": 1.008333, - "weight": 4.2615731761286336e-05 - }, - { - "days": 1.009722, - "weight": 0.003143729750697969 - }, - { - "days": 1.011111, - "weight": 0.00038354158585157704 - }, - { - "days": 1.014583, - "weight": 8.195333031016603e-05 - }, - { - "days": 1.015278, - "weight": 0.002243882183892346 - }, - { - "days": 1.016667, - "weight": 0.002204544585343466 - }, - { - "days": 1.017361, - "weight": 0.001691516737601827 - }, - { - "days": 1.018056, - "weight": 0.020457190312023647 - }, - { - "days": 1.01875, - "weight": 0.0018931219301648353 - }, - { - "days": 1.020139, - "weight": 0.0005654779791401456 - }, - { - "days": 1.021528, - "weight": 0.00017210199365134867 - }, - { - "days": 1.022917, - "weight": 0.0014227098141844823 - }, - { - "days": 1.023611, - "weight": 0.00019832705935060179 - }, - { - "days": 1.024306, - "weight": 0.0003819025192453737 - }, - { - "days": 1.025, - "weight": 0.00025241625735531137 - }, - { - "days": 1.027083, - "weight": 0.0007474143724287142 - }, - { - "days": 1.027778, - "weight": 0.004643475695374008 - }, - { - "days": 1.028472, - "weight": 9.834399637219924e-05 - }, - { - "days": 1.029861, - "weight": 0.0019455720615633417 - }, - { - "days": 1.03125, - "weight": 0.00021635679201883833 - }, - { - "days": 1.031944, - "weight": 0.0022012664521310595 - }, - { - "days": 1.032639, - "weight": 0.0020013003261742544 - }, - { - "days": 1.033333, - "weight": 0.00024094279111188814 - }, - { - "days": 1.034028, - "weight": 0.0012047139555594407 - }, - { - "days": 1.034722, - "weight": 0.005361386868891062 - }, - { - "days": 1.035417, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.036111, - "weight": 0.0012735547530199802 - }, - { - "days": 1.036806, - "weight": 0.000260611590386328 - }, - { - "days": 1.038889, - "weight": 0.0008293677027388802 - }, - { - "days": 1.039583, - "weight": 0.0010588370276073452 - }, - { - "days": 1.040972, - "weight": 0.0005441701132595024 - }, - { - "days": 1.041667, - "weight": 0.0005851467784145854 - }, - { - "days": 1.042361, - "weight": 0.0013227267512060797 - }, - { - "days": 1.043056, - "weight": 0.00210783965557747 - }, - { - "days": 1.04375, - "weight": 0.0012391343542897104 - }, - { - "days": 1.044444, - "weight": 0.0004687730493741497 - }, - { - "days": 1.045139, - "weight": 0.0015259710103752914 - }, - { - "days": 1.045833, - "weight": 0.0006179281105386519 - }, - { - "days": 1.046528, - "weight": 0.0007605269052783408 - }, - { - "days": 1.048611, - "weight": 0.0005966202446580088 - }, - { - "days": 1.05, - "weight": 0.0006457922428441084 - }, - { - "days": 1.050694, - "weight": 0.0004769683824051663 - }, - { - "days": 1.051389, - "weight": 0.02323868634275068 - }, - { - "days": 1.052083, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.052778, - "weight": 5.081106479230294e-05 - }, - { - "days": 1.054861, - "weight": 0.00303555135468855 - }, - { - "days": 1.056944, - "weight": 0.00034584305390890065 - }, - { - "days": 1.058333, - "weight": 0.0005245013139850626 - }, - { - "days": 1.064583, - "weight": 0.000260611590386328 - }, - { - "days": 1.065972, - "weight": 7.211893067294611e-05 - }, - { - "days": 1.069444, - "weight": 0.00046549491616174304 - }, - { - "days": 1.073611, - "weight": 0.00039337598548879697 - }, - { - "days": 1.074306, - "weight": 0.00012948626189006234 - }, - { - "days": 1.076389, - "weight": 0.0003507602537275106 - }, - { - "days": 1.077083, - "weight": 0.0003097835885724276 - }, - { - "days": 1.077778, - "weight": 0.00019668799274439848 - }, - { - "days": 1.079167, - "weight": 9.014866334118263e-05 - }, - { - "days": 1.079861, - "weight": 0.0001671847938327387 - }, - { - "days": 1.08125, - "weight": 0.00010981746261562248 - }, - { - "days": 1.082639, - "weight": 0.0006179281105386519 - }, - { - "days": 1.084028, - "weight": 7.211893067294611e-05 - }, - { - "days": 1.084722, - "weight": 0.0015718648753489846 - }, - { - "days": 1.086111, - "weight": 0.00022783025826226156 - }, - { - "days": 1.0875, - "weight": 7.703613049155608e-05 - }, - { - "days": 1.089583, - "weight": 0.00036223371997093384 - }, - { - "days": 1.090972, - "weight": 0.00020980052559402504 - }, - { - "days": 1.091667, - "weight": 0.0008703443678939632 - }, - { - "days": 1.092361, - "weight": 0.0013178095513874698 - }, - { - "days": 1.09375, - "weight": 0.0005507263796843158 - }, - { - "days": 1.094444, - "weight": 0.00019177079292578852 - }, - { - "days": 1.095139, - "weight": 0.00027864132305456454 - }, - { - "days": 1.097917, - "weight": 0.0015407226098311214 - }, - { - "days": 1.098611, - "weight": 0.00010490026279701252 - }, - { - "days": 1.1, - "weight": 0.00017210199365134867 - }, - { - "days": 1.100694, - "weight": 0.0004917199818609962 - }, - { - "days": 1.101389, - "weight": 0.000470412115980353 - }, - { - "days": 1.102083, - "weight": 0.00040157131851981353 - }, - { - "days": 1.104167, - "weight": 0.00023602559129327818 - }, - { - "days": 1.104861, - "weight": 0.000168823860438942 - }, - { - "days": 1.105556, - "weight": 8.359239691636936e-05 - }, - { - "days": 1.10625, - "weight": 0.0002393037245056848 - }, - { - "days": 1.108333, - "weight": 0.00024258185771809144 - }, - { - "days": 1.109028, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.109722, - "weight": 8.850959673497932e-05 - }, - { - "days": 1.110417, - "weight": 0.000260611590386328 - }, - { - "days": 1.111806, - "weight": 0.00021307865880643167 - }, - { - "days": 1.1125, - "weight": 0.00025733345717392133 - }, - { - "days": 1.113889, - "weight": 0.00015571132758931546 - }, - { - "days": 1.115972, - "weight": 8.359239691636936e-05 - }, - { - "days": 1.116667, - "weight": 0.0001491550611645022 - }, - { - "days": 1.118056, - "weight": 0.0005179450475602494 - }, - { - "days": 1.120139, - "weight": 0.0004802465156175729 - }, - { - "days": 1.120833, - "weight": 0.010386765083510443 - }, - { - "days": 1.131944, - "weight": 7.539706388535275e-05 - }, - { - "days": 1.136111, - "weight": 0.00018685359310717856 - }, - { - "days": 1.136806, - "weight": 0.0002294693248684649 - }, - { - "days": 1.1375, - "weight": 0.0001819363932885686 - }, - { - "days": 1.138889, - "weight": 0.00044910425009970985 - }, - { - "days": 1.140278, - "weight": 5.081106479230294e-05 - }, - { - "days": 1.141667, - "weight": 9.834399637219924e-05 - }, - { - "days": 1.145833, - "weight": 0.0001344034617086723 - }, - { - "days": 1.146528, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.147222, - "weight": 0.00010817839600941917 - }, - { - "days": 1.147917, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.148611, - "weight": 8.195333031016603e-05 - }, - { - "days": 1.149306, - "weight": 0.0001114565292218258 - }, - { - "days": 1.15, - "weight": 0.00012948626189006234 - }, - { - "days": 1.150694, - "weight": 0.00032945238784686746 - }, - { - "days": 1.151389, - "weight": 0.0005753123787773655 - }, - { - "days": 1.152083, - "weight": 0.00011801279564663909 - }, - { - "days": 1.153472, - "weight": 0.00014423786134589223 - }, - { - "days": 1.154861, - "weight": 0.00012784719528385901 - }, - { - "days": 1.155556, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.156944, - "weight": 4.75329315798963e-05 - }, - { - "days": 1.157639, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.159028, - "weight": 0.0004376307838562866 - }, - { - "days": 1.159722, - "weight": 0.0004392698504624899 - }, - { - "days": 1.160417, - "weight": 0.0013718987493921795 - }, - { - "days": 1.161111, - "weight": 0.0002622506569925313 - }, - { - "days": 1.161806, - "weight": 6.720173085433615e-05 - }, - { - "days": 1.163194, - "weight": 0.00014095972813348557 - }, - { - "days": 1.163889, - "weight": 0.00033273052105927406 - }, - { - "days": 1.165278, - "weight": 0.0002999491889352077 - }, - { - "days": 1.165972, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.166667, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.167361, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.168056, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.170139, - "weight": 7.703613049155608e-05 - }, - { - "days": 1.170833, - "weight": 0.00011801279564663909 - }, - { - "days": 1.171528, - "weight": 0.0001491550611645022 - }, - { - "days": 1.172917, - "weight": 6.064546442952286e-05 - }, - { - "days": 1.174306, - "weight": 0.00026388972359873465 - }, - { - "days": 1.176389, - "weight": 0.00021307865880643167 - }, - { - "days": 1.177083, - "weight": 0.0001819363932885686 - }, - { - "days": 1.178472, - "weight": 0.0001032611961908092 - }, - { - "days": 1.181944, - "weight": 6.556266424813282e-05 - }, - { - "days": 1.184722, - "weight": 5.245013139850626e-05 - }, - { - "days": 1.1875, - "weight": 0.0001753801268637553 - }, - { - "days": 1.189583, - "weight": 2.786413230545645e-05 - }, - { - "days": 1.191667, - "weight": 9.014866334118263e-05 - }, - { - "days": 1.193056, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.19375, - "weight": 0.00016226759401412875 - }, - { - "days": 1.194444, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.198611, - "weight": 9.014866334118263e-05 - }, - { - "days": 1.199306, - "weight": 0.0017882216673678229 - }, - { - "days": 1.200694, - "weight": 4.097666515508301e-05 - }, - { - "days": 1.202778, - "weight": 9.998306297840256e-05 - }, - { - "days": 1.203472, - "weight": 6.064546442952286e-05 - }, - { - "days": 1.204861, - "weight": 9.342679655358928e-05 - }, - { - "days": 1.205556, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.206944, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.209028, - "weight": 0.0001753801268637553 - }, - { - "days": 1.209722, - "weight": 8.850959673497932e-05 - }, - { - "days": 1.210417, - "weight": 0.00046549491616174304 - }, - { - "days": 1.211111, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.2125, - "weight": 0.0002540553239615147 - }, - { - "days": 1.213194, - "weight": 0.00010490026279701252 - }, - { - "days": 1.213889, - "weight": 0.00019177079292578852 - }, - { - "days": 1.214583, - "weight": 0.0001360425283148756 - }, - { - "days": 1.215278, - "weight": 0.00012129092885904572 - }, - { - "days": 1.215972, - "weight": 4.75329315798963e-05 - }, - { - "days": 1.218056, - "weight": 0.00013932066152728227 - }, - { - "days": 1.219444, - "weight": 0.00012948626189006234 - }, - { - "days": 1.220139, - "weight": 4.2615731761286336e-05 - }, - { - "days": 1.220833, - "weight": 0.0007933082374024071 - }, - { - "days": 1.222222, - "weight": 0.0003392867874840874 - }, - { - "days": 1.222917, - "weight": 6.720173085433615e-05 - }, - { - "days": 1.224306, - "weight": 0.00012948626189006234 - }, - { - "days": 1.225694, - "weight": 0.0009752446306909758 - }, - { - "days": 1.226389, - "weight": 0.0001114565292218258 - }, - { - "days": 1.228472, - "weight": 0.0001360425283148756 - }, - { - "days": 1.229167, - "weight": 5.081106479230294e-05 - }, - { - "days": 1.229861, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.231944, - "weight": 0.00015243319437690882 - }, - { - "days": 1.232639, - "weight": 9.50658631597926e-05 - }, - { - "days": 1.233333, - "weight": 0.00012292999546524905 - }, - { - "days": 1.234028, - "weight": 0.00015735039419551879 - }, - { - "days": 1.234722, - "weight": 8.031426370396271e-05 - }, - { - "days": 1.2375, - "weight": 0.00042615731761286334 - }, - { - "days": 1.238194, - "weight": 8.359239691636936e-05 - }, - { - "days": 1.238889, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.240278, - "weight": 0.00016554572722653538 - }, - { - "days": 1.246528, - "weight": 0.0017554403352437564 - }, - { - "days": 1.251389, - "weight": 7.047986406674279e-05 - }, - { - "days": 1.254167, - "weight": 0.00017374106025755198 - }, - { - "days": 1.25625, - "weight": 0.00011473466243423244 - }, - { - "days": 1.258333, - "weight": 0.0001196518622528424 - }, - { - "days": 1.261806, - "weight": 0.00025733345717392133 - }, - { - "days": 1.263889, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.264583, - "weight": 3.278133212406641e-05 - }, - { - "days": 1.265972, - "weight": 0.00010981746261562248 - }, - { - "days": 1.267361, - "weight": 0.0002442209243242948 - }, - { - "days": 1.268056, - "weight": 0.00019340985953199182 - }, - { - "days": 1.26875, - "weight": 0.00012620812867765568 - }, - { - "days": 1.270139, - "weight": 9.014866334118263e-05 - }, - { - "days": 1.272222, - "weight": 0.0001032611961908092 - }, - { - "days": 1.273611, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.274306, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.275, - "weight": 0.00013932066152728227 - }, - { - "days": 1.276389, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.277083, - "weight": 0.00015407226098311215 - }, - { - "days": 1.278472, - "weight": 9.50658631597926e-05 - }, - { - "days": 1.279167, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.279861, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.281944, - "weight": 4.097666515508301e-05 - }, - { - "days": 1.282639, - "weight": 0.0003163398549972409 - }, - { - "days": 1.283333, - "weight": 0.00010981746261562248 - }, - { - "days": 1.284028, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.284722, - "weight": 0.0003819025192453737 - }, - { - "days": 1.285417, - "weight": 0.0001425987947396889 - }, - { - "days": 1.286111, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.288194, - "weight": 0.00032945238784686746 - }, - { - "days": 1.290278, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.29375, - "weight": 0.00013768159492107894 - }, - { - "days": 1.294444, - "weight": 0.00011801279564663909 - }, - { - "days": 1.295139, - "weight": 4.2615731761286336e-05 - }, - { - "days": 1.295833, - "weight": 5.245013139850626e-05 - }, - { - "days": 1.296528, - "weight": 0.00017046292704514534 - }, - { - "days": 1.297917, - "weight": 0.00011801279564663909 - }, - { - "days": 1.298611, - "weight": 0.0005294185138036726 - }, - { - "days": 1.299306, - "weight": 0.00015243319437690882 - }, - { - "days": 1.303472, - "weight": 0.00025569439056771803 - }, - { - "days": 1.306944, - "weight": 0.00010653932940321584 - }, - { - "days": 1.313194, - "weight": 9.670492976599591e-05 - }, - { - "days": 1.315972, - "weight": 6.720173085433615e-05 - }, - { - "days": 1.316667, - "weight": 5.081106479230294e-05 - }, - { - "days": 1.317361, - "weight": 4.75329315798963e-05 - }, - { - "days": 1.322917, - "weight": 9.670492976599591e-05 - }, - { - "days": 1.325, - "weight": 7.211893067294611e-05 - }, - { - "days": 1.325694, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.326389, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.327083, - "weight": 9.342679655358928e-05 - }, - { - "days": 1.327778, - "weight": 3.1142265517863095e-05 - }, - { - "days": 1.328472, - "weight": 0.00016554572722653538 - }, - { - "days": 1.329167, - "weight": 5.081106479230294e-05 - }, - { - "days": 1.329861, - "weight": 9.342679655358928e-05 - }, - { - "days": 1.330556, - "weight": 0.00010162212958460587 - }, - { - "days": 1.33125, - "weight": 0.0006539875758751249 - }, - { - "days": 1.332639, - "weight": 0.0001114565292218258 - }, - { - "days": 1.333333, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.334722, - "weight": 0.00012292999546524905 - }, - { - "days": 1.336806, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.338194, - "weight": 0.0001114565292218258 - }, - { - "days": 1.338889, - "weight": 9.014866334118263e-05 - }, - { - "days": 1.339583, - "weight": 8.523146352257267e-05 - }, - { - "days": 1.340278, - "weight": 5.081106479230294e-05 - }, - { - "days": 1.340972, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.342361, - "weight": 0.0003638727865771372 - }, - { - "days": 1.343056, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.344444, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.345139, - "weight": 0.00011801279564663909 - }, - { - "days": 1.346528, - "weight": 0.00019668799274439848 - }, - { - "days": 1.348611, - "weight": 0.000214717725412635 - }, - { - "days": 1.35, - "weight": 0.00018849265971338186 - }, - { - "days": 1.352083, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.352778, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.353472, - "weight": 9.670492976599591e-05 - }, - { - "days": 1.354167, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.354861, - "weight": 6.228453103572619e-05 - }, - { - "days": 1.355556, - "weight": 9.998306297840256e-05 - }, - { - "days": 1.357639, - "weight": 0.0001491550611645022 - }, - { - "days": 1.360417, - "weight": 0.0005228622473788593 - }, - { - "days": 1.361111, - "weight": 0.00010653932940321584 - }, - { - "days": 1.363194, - "weight": 0.00011309559582802913 - }, - { - "days": 1.372222, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.372917, - "weight": 0.00011637372904043576 - }, - { - "days": 1.377083, - "weight": 5.245013139850626e-05 - }, - { - "days": 1.379861, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.382639, - "weight": 9.342679655358928e-05 - }, - { - "days": 1.385417, - "weight": 6.556266424813282e-05 - }, - { - "days": 1.386111, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.386806, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.388194, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.388889, - "weight": 4.2615731761286336e-05 - }, - { - "days": 1.389583, - "weight": 8.359239691636936e-05 - }, - { - "days": 1.390972, - "weight": 6.720173085433615e-05 - }, - { - "days": 1.391667, - "weight": 0.0001032611961908092 - }, - { - "days": 1.392361, - "weight": 0.00012620812867765568 - }, - { - "days": 1.393056, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.395139, - "weight": 4.097666515508301e-05 - }, - { - "days": 1.396528, - "weight": 3.1142265517863095e-05 - }, - { - "days": 1.397222, - "weight": 0.00028847572269178446 - }, - { - "days": 1.397917, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.398611, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.399306, - "weight": 0.0004146838513694401 - }, - { - "days": 1.4, - "weight": 8.031426370396271e-05 - }, - { - "days": 1.402083, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.402778, - "weight": 2.786413230545645e-05 - }, - { - "days": 1.403472, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.404167, - "weight": 0.00030322732214761435 - }, - { - "days": 1.405556, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.407639, - "weight": 0.00010981746261562248 - }, - { - "days": 1.409028, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.409722, - "weight": 3.1142265517863095e-05 - }, - { - "days": 1.410417, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.4125, - "weight": 0.0001491550611645022 - }, - { - "days": 1.413194, - "weight": 0.00011309559582802913 - }, - { - "days": 1.414583, - "weight": 0.0002229130584436516 - }, - { - "days": 1.415972, - "weight": 3.278133212406641e-05 - }, - { - "days": 1.418056, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.41875, - "weight": 5.408919800470958e-05 - }, - { - "days": 1.419444, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.420139, - "weight": 0.00034420398730269734 - }, - { - "days": 1.426389, - "weight": 0.00010817839600941917 - }, - { - "days": 1.430556, - "weight": 8.850959673497932e-05 - }, - { - "days": 1.435417, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.4375, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.440278, - "weight": 4.2615731761286336e-05 - }, - { - "days": 1.441667, - "weight": 4.097666515508301e-05 - }, - { - "days": 1.445833, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.447222, - "weight": 4.2615731761286336e-05 - }, - { - "days": 1.448611, - "weight": 0.0002851975894793778 - }, - { - "days": 1.449306, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.45, - "weight": 5.081106479230294e-05 - }, - { - "days": 1.451389, - "weight": 0.0001425987947396889 - }, - { - "days": 1.453472, - "weight": 7.211893067294611e-05 - }, - { - "days": 1.454167, - "weight": 0.00019832705935060179 - }, - { - "days": 1.45625, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.456944, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.458333, - "weight": 0.00013112532849626565 - }, - { - "days": 1.460417, - "weight": 0.0002950319891165977 - }, - { - "days": 1.461111, - "weight": 0.00026388972359873465 - }, - { - "days": 1.463889, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.464583, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.465972, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.468056, - "weight": 6.228453103572619e-05 - }, - { - "days": 1.469444, - "weight": 6.228453103572619e-05 - }, - { - "days": 1.470139, - "weight": 0.00021307865880643167 - }, - { - "days": 1.471528, - "weight": 0.0001360425283148756 - }, - { - "days": 1.472222, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.472917, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.474306, - "weight": 5.408919800470958e-05 - }, - { - "days": 1.475694, - "weight": 7.539706388535275e-05 - }, - { - "days": 1.476389, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.477778, - "weight": 9.342679655358928e-05 - }, - { - "days": 1.479167, - "weight": 4.097666515508301e-05 - }, - { - "days": 1.48125, - "weight": 0.00038681971906398365 - }, - { - "days": 1.482639, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.495833, - "weight": 6.720173085433615e-05 - }, - { - "days": 1.498611, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.499306, - "weight": 0.00019177079292578852 - }, - { - "days": 1.504167, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.507639, - "weight": 8.523146352257267e-05 - }, - { - "days": 1.508333, - "weight": 8.359239691636936e-05 - }, - { - "days": 1.509028, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.509722, - "weight": 9.670492976599591e-05 - }, - { - "days": 1.510417, - "weight": 3.1142265517863095e-05 - }, - { - "days": 1.513194, - "weight": 0.00010817839600941917 - }, - { - "days": 1.513889, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.515278, - "weight": 3.278133212406641e-05 - }, - { - "days": 1.518056, - "weight": 6.556266424813282e-05 - }, - { - "days": 1.51875, - "weight": 4.097666515508301e-05 - }, - { - "days": 1.519444, - "weight": 0.00010981746261562248 - }, - { - "days": 1.520139, - "weight": 0.0004441870502810999 - }, - { - "days": 1.521528, - "weight": 0.00010162212958460587 - }, - { - "days": 1.522222, - "weight": 0.00015735039419551879 - }, - { - "days": 1.523611, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.524306, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.527083, - "weight": 0.00012292999546524905 - }, - { - "days": 1.527778, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.528472, - "weight": 7.047986406674279e-05 - }, - { - "days": 1.529167, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.530556, - "weight": 4.2615731761286336e-05 - }, - { - "days": 1.53125, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.532639, - "weight": 0.00012129092885904572 - }, - { - "days": 1.534028, - "weight": 6.720173085433615e-05 - }, - { - "days": 1.534722, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.535417, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.536111, - "weight": 7.047986406674279e-05 - }, - { - "days": 1.536806, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.538194, - "weight": 0.0004130447847632368 - }, - { - "days": 1.545833, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.550694, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.55625, - "weight": 4.097666515508301e-05 - }, - { - "days": 1.558333, - "weight": 0.000388458785670187 - }, - { - "days": 1.564583, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.565972, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.567361, - "weight": 5.081106479230294e-05 - }, - { - "days": 1.568056, - "weight": 4.097666515508301e-05 - }, - { - "days": 1.570139, - "weight": 4.097666515508301e-05 - }, - { - "days": 1.572917, - "weight": 0.0001344034617086723 - }, - { - "days": 1.574306, - "weight": 7.211893067294611e-05 - }, - { - "days": 1.577778, - "weight": 4.2615731761286336e-05 - }, - { - "days": 1.578472, - "weight": 4.75329315798963e-05 - }, - { - "days": 1.579167, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.580556, - "weight": 0.00013932066152728227 - }, - { - "days": 1.581944, - "weight": 0.0004556605165245231 - }, - { - "days": 1.582639, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.583333, - "weight": 7.211893067294611e-05 - }, - { - "days": 1.585417, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.586806, - "weight": 3.278133212406641e-05 - }, - { - "days": 1.5875, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.588194, - "weight": 0.00014423786134589223 - }, - { - "days": 1.590972, - "weight": 9.50658631597926e-05 - }, - { - "days": 1.592361, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.593056, - "weight": 8.195333031016603e-05 - }, - { - "days": 1.594444, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.595833, - "weight": 0.00037206811960815376 - }, - { - "days": 1.596528, - "weight": 7.047986406674279e-05 - }, - { - "days": 1.597917, - "weight": 7.211893067294611e-05 - }, - { - "days": 1.599306, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.6, - "weight": 3.1142265517863095e-05 - }, - { - "days": 1.601389, - "weight": 2.786413230545645e-05 - }, - { - "days": 1.615278, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.621528, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.625, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.627083, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.634722, - "weight": 9.50658631597926e-05 - }, - { - "days": 1.6375, - "weight": 3.278133212406641e-05 - }, - { - "days": 1.639583, - "weight": 6.720173085433615e-05 - }, - { - "days": 1.640972, - "weight": 7.047986406674279e-05 - }, - { - "days": 1.641667, - "weight": 3.1142265517863095e-05 - }, - { - "days": 1.642361, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.64375, - "weight": 0.00015407226098311215 - }, - { - "days": 1.644444, - "weight": 8.195333031016603e-05 - }, - { - "days": 1.645833, - "weight": 6.228453103572619e-05 - }, - { - "days": 1.646528, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.647917, - "weight": 0.00020160519256300844 - }, - { - "days": 1.650694, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.653472, - "weight": 9.178772994738595e-05 - }, - { - "days": 1.654861, - "weight": 0.00020160519256300844 - }, - { - "days": 1.655556, - "weight": 0.0001507941277707055 - }, - { - "days": 1.65625, - "weight": 5.408919800470958e-05 - }, - { - "days": 1.656944, - "weight": 0.00010981746261562248 - }, - { - "days": 1.658333, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.659722, - "weight": 0.00043271358403767666 - }, - { - "days": 1.6625, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.668056, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.670833, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.671528, - "weight": 0.00012620812867765568 - }, - { - "days": 1.676389, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.684028, - "weight": 3.1142265517863095e-05 - }, - { - "days": 1.689583, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.691667, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.69375, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.696528, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.697917, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.698611, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.699306, - "weight": 8.523146352257267e-05 - }, - { - "days": 1.700694, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.703472, - "weight": 9.670492976599591e-05 - }, - { - "days": 1.704861, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.705556, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.709722, - "weight": 0.00017865826007616194 - }, - { - "days": 1.711806, - "weight": 0.00010981746261562248 - }, - { - "days": 1.7125, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.713889, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.714583, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.715278, - "weight": 3.1142265517863095e-05 - }, - { - "days": 1.715972, - "weight": 9.342679655358928e-05 - }, - { - "days": 1.716667, - "weight": 0.00017210199365134867 - }, - { - "days": 1.71875, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.720139, - "weight": 0.0007129939736984444 - }, - { - "days": 1.721528, - "weight": 5.9006397823319545e-05 - }, - { - "days": 1.731944, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.735417, - "weight": 0.00010490026279701252 - }, - { - "days": 1.738889, - "weight": 6.720173085433615e-05 - }, - { - "days": 1.743056, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.744444, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.748611, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.757639, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.758333, - "weight": 6.556266424813282e-05 - }, - { - "days": 1.760417, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.761111, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.7625, - "weight": 0.00019504892613819515 - }, - { - "days": 1.763194, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.766667, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.772222, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.773611, - "weight": 0.00013112532849626565 - }, - { - "days": 1.775, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.775694, - "weight": 9.178772994738595e-05 - }, - { - "days": 1.777083, - "weight": 8.687053012877599e-05 - }, - { - "days": 1.777778, - "weight": 9.014866334118263e-05 - }, - { - "days": 1.779167, - "weight": 8.850959673497932e-05 - }, - { - "days": 1.781944, - "weight": 0.00018029732668236527 - }, - { - "days": 1.782639, - "weight": 0.0006113718441138386 - }, - { - "days": 1.789583, - "weight": 0.00011473466243423244 - }, - { - "days": 1.810417, - "weight": 5.408919800470958e-05 - }, - { - "days": 1.813194, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.815278, - "weight": 3.605946533647306e-05 - }, - { - "days": 1.819444, - "weight": 9.998306297840256e-05 - }, - { - "days": 1.820833, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.821528, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.823611, - "weight": 8.195333031016603e-05 - }, - { - "days": 1.825, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.825694, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.827083, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.828472, - "weight": 8.850959673497932e-05 - }, - { - "days": 1.829167, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.83125, - "weight": 5.245013139850626e-05 - }, - { - "days": 1.831944, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.834028, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.834722, - "weight": 0.00010162212958460587 - }, - { - "days": 1.835417, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.836806, - "weight": 0.0001835754598947719 - }, - { - "days": 1.8375, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.838889, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.839583, - "weight": 4.75329315798963e-05 - }, - { - "days": 1.840278, - "weight": 8.359239691636936e-05 - }, - { - "days": 1.840972, - "weight": 0.0001425987947396889 - }, - { - "days": 1.844444, - "weight": 0.000634318776600685 - }, - { - "days": 1.845833, - "weight": 4.2615731761286336e-05 - }, - { - "days": 1.847917, - "weight": 7.539706388535275e-05 - }, - { - "days": 1.849306, - "weight": 5.245013139850626e-05 - }, - { - "days": 1.85625, - "weight": 8.850959673497932e-05 - }, - { - "days": 1.857639, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.863889, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.867361, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.868056, - "weight": 5.245013139850626e-05 - }, - { - "days": 1.872222, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.872917, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.878472, - "weight": 0.00010490026279701252 - }, - { - "days": 1.880556, - "weight": 4.425479836748966e-05 - }, - { - "days": 1.88125, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.882639, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.883333, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.884722, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.885417, - "weight": 3.278133212406641e-05 - }, - { - "days": 1.886806, - "weight": 3.278133212406641e-05 - }, - { - "days": 1.888194, - "weight": 6.064546442952286e-05 - }, - { - "days": 1.890278, - "weight": 0.00025241625735531137 - }, - { - "days": 1.891667, - "weight": 0.00010162212958460587 - }, - { - "days": 1.894444, - "weight": 5.245013139850626e-05 - }, - { - "days": 1.895833, - "weight": 8.195333031016603e-05 - }, - { - "days": 1.897222, - "weight": 7.047986406674279e-05 - }, - { - "days": 1.897917, - "weight": 0.00025241625735531137 - }, - { - "days": 1.898611, - "weight": 8.195333031016603e-05 - }, - { - "days": 1.899306, - "weight": 0.00017374106025755198 - }, - { - "days": 1.9, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.900694, - "weight": 0.00012456906207145238 - }, - { - "days": 1.906944, - "weight": 9.998306297840256e-05 - }, - { - "days": 1.907639, - "weight": 0.0009834399637219924 - }, - { - "days": 1.908333, - "weight": 5.081106479230294e-05 - }, - { - "days": 1.909722, - "weight": 5.245013139850626e-05 - }, - { - "days": 1.916667, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.921528, - "weight": 2.786413230545645e-05 - }, - { - "days": 1.922917, - "weight": 7.047986406674279e-05 - }, - { - "days": 1.923611, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.925694, - "weight": 3.1142265517863095e-05 - }, - { - "days": 1.932639, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.933333, - "weight": 0.00012129092885904572 - }, - { - "days": 1.936111, - "weight": 4.097666515508301e-05 - }, - { - "days": 1.939583, - "weight": 5.245013139850626e-05 - }, - { - "days": 1.940278, - "weight": 0.00011473466243423244 - }, - { - "days": 1.941667, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.942361, - "weight": 9.670492976599591e-05 - }, - { - "days": 1.943056, - "weight": 5.408919800470958e-05 - }, - { - "days": 1.945139, - "weight": 5.408919800470958e-05 - }, - { - "days": 1.945833, - "weight": 0.00010162212958460587 - }, - { - "days": 1.946528, - "weight": 0.00016062852740792542 - }, - { - "days": 1.947222, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.947917, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.949306, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.95, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.950694, - "weight": 9.178772994738595e-05 - }, - { - "days": 1.951389, - "weight": 9.998306297840256e-05 - }, - { - "days": 1.954861, - "weight": 0.00021143959220022837 - }, - { - "days": 1.955556, - "weight": 3.7698531942676374e-05 - }, - { - "days": 1.956944, - "weight": 0.00014423786134589223 - }, - { - "days": 1.958333, - "weight": 6.228453103572619e-05 - }, - { - "days": 1.959028, - "weight": 0.0004163229179756434 - }, - { - "days": 1.959722, - "weight": 0.000168823860438942 - }, - { - "days": 1.961111, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.963194, - "weight": 2.9503198911659772e-05 - }, - { - "days": 1.963889, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.964583, - "weight": 0.0002081614589878217 - }, - { - "days": 1.965972, - "weight": 0.00041140571815703346 - }, - { - "days": 1.966667, - "weight": 4.917199818609962e-05 - }, - { - "days": 1.968056, - "weight": 4.75329315798963e-05 - }, - { - "days": 1.970139, - "weight": 5.57282646109129e-05 - }, - { - "days": 1.970833, - "weight": 4.75329315798963e-05 - }, - { - "days": 1.972917, - "weight": 9.50658631597926e-05 - }, - { - "days": 1.975694, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.98125, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.981944, - "weight": 7.375799727914943e-05 - }, - { - "days": 1.982639, - "weight": 0.0001819363932885686 - }, - { - "days": 1.9875, - "weight": 9.50658631597926e-05 - }, - { - "days": 1.988194, - "weight": 9.998306297840256e-05 - }, - { - "days": 1.990972, - "weight": 6.064546442952286e-05 - }, - { - "days": 1.99375, - "weight": 0.00010817839600941917 - }, - { - "days": 1.997222, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.997917, - "weight": 6.392359764192951e-05 - }, - { - "days": 1.998611, - "weight": 0.00025241625735531137 - }, - { - "days": 1.999306, - "weight": 8.523146352257267e-05 - }, - { - "days": 2.001389, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.002083, - "weight": 0.00011637372904043576 - }, - { - "days": 2.004167, - "weight": 0.00038354158585157704 - }, - { - "days": 2.004861, - "weight": 0.00019668799274439848 - }, - { - "days": 2.005556, - "weight": 3.4420398730269734e-05 - }, - { - "days": 2.006944, - "weight": 3.605946533647306e-05 - }, - { - "days": 2.008333, - "weight": 8.195333031016603e-05 - }, - { - "days": 2.009028, - "weight": 0.00019340985953199182 - }, - { - "days": 2.009722, - "weight": 0.0001671847938327387 - }, - { - "days": 2.010417, - "weight": 0.00013768159492107894 - }, - { - "days": 2.013194, - "weight": 4.097666515508301e-05 - }, - { - "days": 2.014583, - "weight": 6.884079746053947e-05 - }, - { - "days": 2.015278, - "weight": 5.408919800470958e-05 - }, - { - "days": 2.015972, - "weight": 0.0002622506569925313 - }, - { - "days": 2.018056, - "weight": 5.9006397823319545e-05 - }, - { - "days": 2.01875, - "weight": 9.834399637219924e-05 - }, - { - "days": 2.020139, - "weight": 0.00012129092885904572 - }, - { - "days": 2.020833, - "weight": 0.000342564920696494 - }, - { - "days": 2.023611, - "weight": 0.00012129092885904572 - }, - { - "days": 2.024306, - "weight": 6.064546442952286e-05 - }, - { - "days": 2.025694, - "weight": 0.0005130278477416394 - }, - { - "days": 2.03125, - "weight": 0.00030650545536002095 - }, - { - "days": 2.031944, - "weight": 5.736733121711622e-05 - }, - { - "days": 2.034028, - "weight": 9.342679655358928e-05 - }, - { - "days": 2.041667, - "weight": 6.392359764192951e-05 - }, - { - "days": 2.042361, - "weight": 9.342679655358928e-05 - }, - { - "days": 2.044444, - "weight": 4.917199818609962e-05 - }, - { - "days": 2.045139, - "weight": 5.081106479230294e-05 - }, - { - "days": 2.046528, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.048611, - "weight": 4.75329315798963e-05 - }, - { - "days": 2.049306, - "weight": 7.375799727914943e-05 - }, - { - "days": 2.052778, - "weight": 8.687053012877599e-05 - }, - { - "days": 2.053472, - "weight": 4.917199818609962e-05 - }, - { - "days": 2.054167, - "weight": 3.605946533647306e-05 - }, - { - "days": 2.056944, - "weight": 9.998306297840256e-05 - }, - { - "days": 2.057639, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.059028, - "weight": 3.605946533647306e-05 - }, - { - "days": 2.059722, - "weight": 7.375799727914943e-05 - }, - { - "days": 2.063889, - "weight": 0.00011637372904043576 - }, - { - "days": 2.065278, - "weight": 6.064546442952286e-05 - }, - { - "days": 2.065972, - "weight": 3.605946533647306e-05 - }, - { - "days": 2.066667, - "weight": 7.047986406674279e-05 - }, - { - "days": 2.068056, - "weight": 0.00010817839600941917 - }, - { - "days": 2.070833, - "weight": 0.00016554572722653538 - }, - { - "days": 2.071528, - "weight": 6.392359764192951e-05 - }, - { - "days": 2.072917, - "weight": 7.867519709775939e-05 - }, - { - "days": 2.073611, - "weight": 4.917199818609962e-05 - }, - { - "days": 2.075, - "weight": 0.00016554572722653538 - }, - { - "days": 2.077083, - "weight": 0.00011801279564663909 - }, - { - "days": 2.078472, - "weight": 9.998306297840256e-05 - }, - { - "days": 2.079861, - "weight": 8.523146352257267e-05 - }, - { - "days": 2.080556, - "weight": 4.425479836748966e-05 - }, - { - "days": 2.082639, - "weight": 7.047986406674279e-05 - }, - { - "days": 2.083333, - "weight": 2.786413230545645e-05 - }, - { - "days": 2.086806, - "weight": 0.00036059465336473053 - }, - { - "days": 2.093056, - "weight": 0.0005081106479230295 - }, - { - "days": 2.095139, - "weight": 3.4420398730269734e-05 - }, - { - "days": 2.108333, - "weight": 7.867519709775939e-05 - }, - { - "days": 2.110417, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.115972, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.117361, - "weight": 2.786413230545645e-05 - }, - { - "days": 2.11875, - "weight": 0.00017374106025755198 - }, - { - "days": 2.120833, - "weight": 5.736733121711622e-05 - }, - { - "days": 2.122917, - "weight": 2.9503198911659772e-05 - }, - { - "days": 2.125, - "weight": 4.425479836748966e-05 - }, - { - "days": 2.126389, - "weight": 3.4420398730269734e-05 - }, - { - "days": 2.127778, - "weight": 6.556266424813282e-05 - }, - { - "days": 2.129861, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.130556, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.132639, - "weight": 2.9503198911659772e-05 - }, - { - "days": 2.134028, - "weight": 5.9006397823319545e-05 - }, - { - "days": 2.136111, - "weight": 0.00010490026279701252 - }, - { - "days": 2.1375, - "weight": 0.0001507941277707055 - }, - { - "days": 2.138194, - "weight": 8.523146352257267e-05 - }, - { - "days": 2.138889, - "weight": 4.2615731761286336e-05 - }, - { - "days": 2.140278, - "weight": 0.00015571132758931546 - }, - { - "days": 2.140972, - "weight": 4.75329315798963e-05 - }, - { - "days": 2.145139, - "weight": 4.2615731761286336e-05 - }, - { - "days": 2.15, - "weight": 0.00012292999546524905 - }, - { - "days": 2.152778, - "weight": 2.9503198911659772e-05 - }, - { - "days": 2.154861, - "weight": 0.00018685359310717856 - }, - { - "days": 2.159028, - "weight": 0.00010162212958460587 - }, - { - "days": 2.164583, - "weight": 3.605946533647306e-05 - }, - { - "days": 2.165972, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.168056, - "weight": 3.1142265517863095e-05 - }, - { - "days": 2.178472, - "weight": 2.786413230545645e-05 - }, - { - "days": 2.180556, - "weight": 5.9006397823319545e-05 - }, - { - "days": 2.181944, - "weight": 0.00010817839600941917 - }, - { - "days": 2.182639, - "weight": 5.408919800470958e-05 - }, - { - "days": 2.184722, - "weight": 0.00010981746261562248 - }, - { - "days": 2.188194, - "weight": 3.4420398730269734e-05 - }, - { - "days": 2.189583, - "weight": 6.064546442952286e-05 - }, - { - "days": 2.190972, - "weight": 6.556266424813282e-05 - }, - { - "days": 2.191667, - "weight": 3.605946533647306e-05 - }, - { - "days": 2.195139, - "weight": 6.228453103572619e-05 - }, - { - "days": 2.196528, - "weight": 2.9503198911659772e-05 - }, - { - "days": 2.197222, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.197917, - "weight": 3.605946533647306e-05 - }, - { - "days": 2.198611, - "weight": 6.720173085433615e-05 - }, - { - "days": 2.199306, - "weight": 5.57282646109129e-05 - }, - { - "days": 2.2, - "weight": 6.392359764192951e-05 - }, - { - "days": 2.204167, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.206944, - "weight": 0.00013932066152728227 - }, - { - "days": 2.210417, - "weight": 0.000260611590386328 - }, - { - "days": 2.211806, - "weight": 2.786413230545645e-05 - }, - { - "days": 2.220833, - "weight": 0.00013276439510246898 - }, - { - "days": 2.238889, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.241667, - "weight": 4.75329315798963e-05 - }, - { - "days": 2.24375, - "weight": 6.720173085433615e-05 - }, - { - "days": 2.245139, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.246528, - "weight": 5.081106479230294e-05 - }, - { - "days": 2.247917, - "weight": 9.178772994738595e-05 - }, - { - "days": 2.250694, - "weight": 5.9006397823319545e-05 - }, - { - "days": 2.252778, - "weight": 5.245013139850626e-05 - }, - { - "days": 2.253472, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.254167, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.25625, - "weight": 7.867519709775939e-05 - }, - { - "days": 2.256944, - "weight": 0.00012784719528385901 - }, - { - "days": 2.258333, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.259028, - "weight": 5.081106479230294e-05 - }, - { - "days": 2.261806, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.266667, - "weight": 9.670492976599591e-05 - }, - { - "days": 2.269444, - "weight": 0.00021635679201883833 - }, - { - "days": 2.272222, - "weight": 0.00018029732668236527 - }, - { - "days": 2.299306, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.302778, - "weight": 5.57282646109129e-05 - }, - { - "days": 2.309722, - "weight": 4.2615731761286336e-05 - }, - { - "days": 2.311806, - "weight": 6.884079746053947e-05 - }, - { - "days": 2.3125, - "weight": 5.736733121711622e-05 - }, - { - "days": 2.313194, - "weight": 3.1142265517863095e-05 - }, - { - "days": 2.314583, - "weight": 6.064546442952286e-05 - }, - { - "days": 2.315972, - "weight": 6.064546442952286e-05 - }, - { - "days": 2.318056, - "weight": 0.00012292999546524905 - }, - { - "days": 2.31875, - "weight": 5.245013139850626e-05 - }, - { - "days": 2.325694, - "weight": 9.342679655358928e-05 - }, - { - "days": 2.330556, - "weight": 0.0002442209243242948 - }, - { - "days": 2.33125, - "weight": 0.00012129092885904572 - }, - { - "days": 2.336111, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.338194, - "weight": 0.00011801279564663909 - }, - { - "days": 2.360417, - "weight": 5.245013139850626e-05 - }, - { - "days": 2.36875, - "weight": 4.75329315798963e-05 - }, - { - "days": 2.373611, - "weight": 0.00010162212958460587 - }, - { - "days": 2.375, - "weight": 3.1142265517863095e-05 - }, - { - "days": 2.376389, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.379167, - "weight": 7.539706388535275e-05 - }, - { - "days": 2.379861, - "weight": 3.1142265517863095e-05 - }, - { - "days": 2.384722, - "weight": 0.00016554572722653538 - }, - { - "days": 2.385417, - "weight": 4.75329315798963e-05 - }, - { - "days": 2.386111, - "weight": 0.0002589725237801247 - }, - { - "days": 2.392361, - "weight": 0.00013932066152728227 - }, - { - "days": 2.39375, - "weight": 2.9503198911659772e-05 - }, - { - "days": 2.397222, - "weight": 7.703613049155608e-05 - }, - { - "days": 2.421528, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.43125, - "weight": 7.703613049155608e-05 - }, - { - "days": 2.432639, - "weight": 6.064546442952286e-05 - }, - { - "days": 2.434722, - "weight": 7.867519709775939e-05 - }, - { - "days": 2.435417, - "weight": 2.786413230545645e-05 - }, - { - "days": 2.436806, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.4375, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.440278, - "weight": 0.00017210199365134867 - }, - { - "days": 2.44375, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.447917, - "weight": 0.0001196518622528424 - }, - { - "days": 2.448611, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.450694, - "weight": 0.00026716785681114125 - }, - { - "days": 2.451389, - "weight": 5.57282646109129e-05 - }, - { - "days": 2.458333, - "weight": 0.0001360425283148756 - }, - { - "days": 2.478472, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.49375, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.495139, - "weight": 2.9503198911659772e-05 - }, - { - "days": 2.495833, - "weight": 7.211893067294611e-05 - }, - { - "days": 2.496528, - "weight": 6.228453103572619e-05 - }, - { - "days": 2.499306, - "weight": 0.00026716785681114125 - }, - { - "days": 2.501389, - "weight": 4.097666515508301e-05 - }, - { - "days": 2.505556, - "weight": 0.00035895558675852723 - }, - { - "days": 2.509722, - "weight": 6.392359764192951e-05 - }, - { - "days": 2.511111, - "weight": 4.917199818609962e-05 - }, - { - "days": 2.5125, - "weight": 0.00013768159492107894 - }, - { - "days": 2.513194, - "weight": 4.425479836748966e-05 - }, - { - "days": 2.513889, - "weight": 0.00010490026279701252 - }, - { - "days": 2.532639, - "weight": 3.605946533647306e-05 - }, - { - "days": 2.538194, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.552083, - "weight": 2.9503198911659772e-05 - }, - { - "days": 2.554167, - "weight": 5.081106479230294e-05 - }, - { - "days": 2.557639, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.561111, - "weight": 3.1142265517863095e-05 - }, - { - "days": 2.5625, - "weight": 5.9006397823319545e-05 - }, - { - "days": 2.565278, - "weight": 7.047986406674279e-05 - }, - { - "days": 2.565972, - "weight": 0.0004589386497369298 - }, - { - "days": 2.567361, - "weight": 0.0002065223923816184 - }, - { - "days": 2.568056, - "weight": 5.408919800470958e-05 - }, - { - "days": 2.570139, - "weight": 8.031426370396271e-05 - }, - { - "days": 2.570833, - "weight": 0.00019504892613819515 - }, - { - "days": 2.578472, - "weight": 0.00014587692795209553 - }, - { - "days": 2.602083, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.620139, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.621528, - "weight": 0.00017865826007616194 - }, - { - "days": 2.628472, - "weight": 0.0001819363932885686 - }, - { - "days": 2.629167, - "weight": 9.178772994738595e-05 - }, - { - "days": 2.629861, - "weight": 0.0001196518622528424 - }, - { - "days": 2.63125, - "weight": 6.228453103572619e-05 - }, - { - "days": 2.632639, - "weight": 0.00018029732668236527 - }, - { - "days": 2.633333, - "weight": 0.00015898946080172212 - }, - { - "days": 2.636806, - "weight": 0.000562199845927739 - }, - { - "days": 2.672917, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.674306, - "weight": 5.9006397823319545e-05 - }, - { - "days": 2.680556, - "weight": 0.0002753631898421579 - }, - { - "days": 2.684722, - "weight": 0.0001114565292218258 - }, - { - "days": 2.686806, - "weight": 8.195333031016603e-05 - }, - { - "days": 2.688194, - "weight": 0.00048680278204238624 - }, - { - "days": 2.689583, - "weight": 5.736733121711622e-05 - }, - { - "days": 2.690278, - "weight": 0.00013276439510246898 - }, - { - "days": 2.69375, - "weight": 3.605946533647306e-05 - }, - { - "days": 2.694444, - "weight": 9.178772994738595e-05 - }, - { - "days": 2.698611, - "weight": 0.00012948626189006234 - }, - { - "days": 2.700694, - "weight": 4.097666515508301e-05 - }, - { - "days": 2.713889, - "weight": 2.786413230545645e-05 - }, - { - "days": 2.729861, - "weight": 5.57282646109129e-05 - }, - { - "days": 2.731944, - "weight": 2.786413230545645e-05 - }, - { - "days": 2.739583, - "weight": 0.00020324425916921175 - }, - { - "days": 2.747222, - "weight": 0.0007113549070922411 - }, - { - "days": 2.748611, - "weight": 7.211893067294611e-05 - }, - { - "days": 2.75, - "weight": 8.195333031016603e-05 - }, - { - "days": 2.750694, - "weight": 5.245013139850626e-05 - }, - { - "days": 2.751389, - "weight": 5.408919800470958e-05 - }, - { - "days": 2.754861, - "weight": 0.00010981746261562248 - }, - { - "days": 2.75625, - "weight": 0.00017865826007616194 - }, - { - "days": 2.757639, - "weight": 2.9503198911659772e-05 - }, - { - "days": 2.780556, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.793056, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.794444, - "weight": 5.081106479230294e-05 - }, - { - "days": 2.804861, - "weight": 6.720173085433615e-05 - }, - { - "days": 2.805556, - "weight": 3.1142265517863095e-05 - }, - { - "days": 2.80625, - "weight": 0.0012915844856882168 - }, - { - "days": 2.807639, - "weight": 8.687053012877599e-05 - }, - { - "days": 2.809028, - "weight": 2.9503198911659772e-05 - }, - { - "days": 2.813889, - "weight": 0.00037206811960815376 - }, - { - "days": 2.816667, - "weight": 8.687053012877599e-05 - }, - { - "days": 2.822222, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.848611, - "weight": 5.736733121711622e-05 - }, - { - "days": 2.849306, - "weight": 5.736733121711622e-05 - }, - { - "days": 2.859028, - "weight": 3.1142265517863095e-05 - }, - { - "days": 2.864583, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.867361, - "weight": 7.539706388535275e-05 - }, - { - "days": 2.868056, - "weight": 4.75329315798963e-05 - }, - { - "days": 2.870833, - "weight": 7.703613049155608e-05 - }, - { - "days": 2.871528, - "weight": 0.0001753801268637553 - }, - { - "days": 2.873611, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.875, - "weight": 3.605946533647306e-05 - }, - { - "days": 2.875694, - "weight": 0.000296671055722801 - }, - { - "days": 2.880556, - "weight": 0.006189115505023738 - }, - { - "days": 2.882639, - "weight": 0.00014423786134589223 - }, - { - "days": 2.890972, - "weight": 6.720173085433615e-05 - }, - { - "days": 2.895833, - "weight": 5.736733121711622e-05 - }, - { - "days": 2.913194, - "weight": 0.0001032611961908092 - }, - { - "days": 2.914583, - "weight": 0.00015898946080172212 - }, - { - "days": 2.91875, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.919444, - "weight": 5.081106479230294e-05 - }, - { - "days": 2.922917, - "weight": 0.00017046292704514534 - }, - { - "days": 2.925694, - "weight": 0.00030322732214761435 - }, - { - "days": 2.929167, - "weight": 0.000260611590386328 - }, - { - "days": 2.934028, - "weight": 0.00024094279111188814 - }, - { - "days": 2.9375, - "weight": 6.556266424813282e-05 - }, - { - "days": 2.940972, - "weight": 0.0001360425283148756 - }, - { - "days": 2.941667, - "weight": 0.0008785397009249799 - }, - { - "days": 2.942361, - "weight": 0.003900978522763903 - }, - { - "days": 2.95, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.954167, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.957639, - "weight": 8.031426370396271e-05 - }, - { - "days": 2.959028, - "weight": 4.2615731761286336e-05 - }, - { - "days": 2.959722, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.960417, - "weight": 3.7698531942676374e-05 - }, - { - "days": 2.965278, - "weight": 3.1142265517863095e-05 - }, - { - "days": 2.965972, - "weight": 4.2615731761286336e-05 - }, - { - "days": 2.968056, - "weight": 0.0003491211871213073 - }, - { - "days": 2.969444, - "weight": 4.917199818609962e-05 - }, - { - "days": 2.971528, - "weight": 3.4420398730269734e-05 - }, - { - "days": 2.972222, - "weight": 0.0004196010511880501 - }, - { - "days": 2.972917, - "weight": 4.2615731761286336e-05 - }, - { - "days": 2.974306, - "weight": 0.00010981746261562248 - }, - { - "days": 2.976389, - "weight": 3.278133212406641e-05 - }, - { - "days": 2.977083, - "weight": 5.57282646109129e-05 - }, - { - "days": 2.978472, - "weight": 6.392359764192951e-05 - }, - { - "days": 2.982639, - "weight": 6.392359764192951e-05 - }, - { - "days": 2.984722, - "weight": 0.00024094279111188814 - }, - { - "days": 2.985417, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.986806, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.9875, - "weight": 0.00044746518349350655 - }, - { - "days": 2.990972, - "weight": 0.0007441362392163075 - }, - { - "days": 2.993056, - "weight": 0.000562199845927739 - }, - { - "days": 2.995139, - "weight": 0.000521223180772656 - }, - { - "days": 2.997917, - "weight": 0.0002212739918374483 - }, - { - "days": 3.0, - "weight": 0.0004212401177942534 - }, - { - "days": 3.008333, - "weight": 0.0022143789849806863 - }, - { - "days": 3.011111, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.015278, - "weight": 8.031426370396271e-05 - }, - { - "days": 3.017361, - "weight": 5.57282646109129e-05 - }, - { - "days": 3.030556, - "weight": 6.392359764192951e-05 - }, - { - "days": 3.031944, - "weight": 4.75329315798963e-05 - }, - { - "days": 3.032639, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.033333, - "weight": 0.00016554572722653538 - }, - { - "days": 3.036111, - "weight": 0.00013112532849626565 - }, - { - "days": 3.0375, - "weight": 5.245013139850626e-05 - }, - { - "days": 3.038194, - "weight": 0.00029011478929798776 - }, - { - "days": 3.042361, - "weight": 0.0005998983778704154 - }, - { - "days": 3.04375, - "weight": 8.031426370396271e-05 - }, - { - "days": 3.048611, - "weight": 0.00044746518349350655 - }, - { - "days": 3.049306, - "weight": 0.00012784719528385901 - }, - { - "days": 3.050694, - "weight": 0.00031306172178483427 - }, - { - "days": 3.052083, - "weight": 2.9503198911659772e-05 - }, - { - "days": 3.055556, - "weight": 6.392359764192951e-05 - }, - { - "days": 3.056944, - "weight": 8.523146352257267e-05 - }, - { - "days": 3.059722, - "weight": 0.0003737071862143571 - }, - { - "days": 3.061111, - "weight": 0.00021143959220022837 - }, - { - "days": 3.090972, - "weight": 8.523146352257267e-05 - }, - { - "days": 3.095139, - "weight": 3.1142265517863095e-05 - }, - { - "days": 3.098611, - "weight": 3.4420398730269734e-05 - }, - { - "days": 3.104167, - "weight": 4.75329315798963e-05 - }, - { - "days": 3.105556, - "weight": 3.9337598548879697e-05 - }, - { - "days": 3.106944, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.107639, - "weight": 0.00017374106025755198 - }, - { - "days": 3.109028, - "weight": 5.408919800470958e-05 - }, - { - "days": 3.1125, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.114583, - "weight": 0.00013768159492107894 - }, - { - "days": 3.115278, - "weight": 6.556266424813282e-05 - }, - { - "days": 3.115972, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.116667, - "weight": 6.884079746053947e-05 - }, - { - "days": 3.123611, - "weight": 6.884079746053947e-05 - }, - { - "days": 3.138194, - "weight": 3.1142265517863095e-05 - }, - { - "days": 3.15, - "weight": 3.4420398730269734e-05 - }, - { - "days": 3.156944, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.157639, - "weight": 4.2615731761286336e-05 - }, - { - "days": 3.165278, - "weight": 7.539706388535275e-05 - }, - { - "days": 3.169444, - "weight": 5.081106479230294e-05 - }, - { - "days": 3.170139, - "weight": 4.2615731761286336e-05 - }, - { - "days": 3.174306, - "weight": 3.4420398730269734e-05 - }, - { - "days": 3.175, - "weight": 7.375799727914943e-05 - }, - { - "days": 3.177778, - "weight": 2.9503198911659772e-05 - }, - { - "days": 3.18125, - "weight": 0.00012620812867765568 - }, - { - "days": 3.213194, - "weight": 0.000168823860438942 - }, - { - "days": 3.224306, - "weight": 9.998306297840256e-05 - }, - { - "days": 3.23125, - "weight": 5.57282646109129e-05 - }, - { - "days": 3.233333, - "weight": 3.4420398730269734e-05 - }, - { - "days": 3.234028, - "weight": 4.917199818609962e-05 - }, - { - "days": 3.235417, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.236111, - "weight": 2.9503198911659772e-05 - }, - { - "days": 3.275, - "weight": 5.245013139850626e-05 - }, - { - "days": 3.2875, - "weight": 9.998306297840256e-05 - }, - { - "days": 3.291667, - "weight": 3.1142265517863095e-05 - }, - { - "days": 3.293056, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.297917, - "weight": 2.9503198911659772e-05 - }, - { - "days": 3.300694, - "weight": 0.0001196518622528424 - }, - { - "days": 3.343056, - "weight": 6.064546442952286e-05 - }, - { - "days": 3.347222, - "weight": 4.917199818609962e-05 - }, - { - "days": 3.349306, - "weight": 5.245013139850626e-05 - }, - { - "days": 3.359028, - "weight": 3.4420398730269734e-05 - }, - { - "days": 3.404167, - "weight": 3.7698531942676374e-05 - }, - { - "days": 3.406944, - "weight": 3.1142265517863095e-05 - }, - { - "days": 3.409028, - "weight": 3.605946533647306e-05 - }, - { - "days": 3.417361, - "weight": 9.014866334118263e-05 - }, - { - "days": 3.419444, - "weight": 4.75329315798963e-05 - }, - { - "days": 3.478472, - "weight": 4.5893864973692975e-05 - }, - { - "days": 3.479167, - "weight": 3.4420398730269734e-05 - }, - { - "days": 3.485417, - "weight": 4.425479836748966e-05 - }, - { - "days": 3.524306, - "weight": 3.1142265517863095e-05 - }, - { - "days": 3.533333, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.544444, - "weight": 4.917199818609962e-05 - }, - { - "days": 3.545139, - "weight": 8.687053012877599e-05 - }, - { - "days": 3.590278, - "weight": 4.097666515508301e-05 - }, - { - "days": 3.611111, - "weight": 9.014866334118263e-05 - }, - { - "days": 3.660417, - "weight": 4.2615731761286336e-05 - }, - { - "days": 3.670139, - "weight": 0.00016390666062033205 - }, - { - "days": 3.719444, - "weight": 5.408919800470958e-05 - }, - { - "days": 3.723611, - "weight": 3.1142265517863095e-05 - }, - { - "days": 3.727778, - "weight": 0.00019340985953199182 - }, - { - "days": 3.757639, - "weight": 3.7698531942676374e-05 - }, - { - "days": 3.767361, - "weight": 4.2615731761286336e-05 - }, - { - "days": 3.779167, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.782639, - "weight": 2.9503198911659772e-05 - }, - { - "days": 3.784722, - "weight": 4.2615731761286336e-05 - }, - { - "days": 3.786111, - "weight": 0.00021799585862504163 - }, - { - "days": 3.798611, - "weight": 2.786413230545645e-05 - }, - { - "days": 3.804861, - "weight": 3.1142265517863095e-05 - }, - { - "days": 3.809028, - "weight": 4.425479836748966e-05 - }, - { - "days": 3.822222, - "weight": 4.75329315798963e-05 - }, - { - "days": 3.831944, - "weight": 4.097666515508301e-05 - }, - { - "days": 3.839583, - "weight": 3.1142265517863095e-05 - }, - { - "days": 3.840278, - "weight": 3.278133212406641e-05 - }, - { - "days": 3.849306, - "weight": 0.00042451825100666004 - }, - { - "days": 3.857639, - "weight": 4.5893864973692975e-05 - }, - { - "days": 3.86875, - "weight": 3.605946533647306e-05 - }, - { - "days": 3.884028, - "weight": 4.425479836748966e-05 - }, - { - "days": 3.89375, - "weight": 4.5893864973692975e-05 - }, - { - "days": 3.898611, - "weight": 0.0008310067693450835 - }, - { - "days": 3.899306, - "weight": 3.1142265517863095e-05 - }, - { - "days": 3.927083, - "weight": 8.195333031016603e-05 - }, - { - "days": 3.930556, - "weight": 6.228453103572619e-05 - }, - { - "days": 3.931944, - "weight": 0.0001753801268637553 - }, - { - "days": 3.932639, - "weight": 5.57282646109129e-05 - }, - { - "days": 3.943056, - "weight": 6.720173085433615e-05 - }, - { - "days": 3.94375, - "weight": 3.605946533647306e-05 - }, - { - "days": 3.945833, - "weight": 6.228453103572619e-05 - }, - { - "days": 3.952083, - "weight": 3.4420398730269734e-05 - }, - { - "days": 3.952778, - "weight": 0.0001114565292218258 - }, - { - "days": 3.953472, - "weight": 5.081106479230294e-05 - }, - { - "days": 3.95625, - "weight": 3.9337598548879697e-05 - }, - { - "days": 3.959722, - "weight": 0.00012129092885904572 - }, - { - "days": 3.961111, - "weight": 3.9337598548879697e-05 - }, - { - "days": 3.963889, - "weight": 8.031426370396271e-05 - }, - { - "days": 3.965278, - "weight": 0.00039665411870120357 - }, - { - "days": 3.968056, - "weight": 3.605946533647306e-05 - }, - { - "days": 3.984028, - "weight": 3.605946533647306e-05 - }, - { - "days": 3.99375, - "weight": 0.00015735039419551879 - }, - { - "days": 3.995833, - "weight": 8.359239691636936e-05 - }, - { - "days": 3.996528, - "weight": 6.064546442952286e-05 - }, - { - "days": 3.998611, - "weight": 8.031426370396271e-05 - }, - { - "days": 4.001389, - "weight": 0.00019504892613819515 - }, - { - "days": 4.002778, - "weight": 3.605946533647306e-05 - }, - { - "days": 4.004167, - "weight": 4.5893864973692975e-05 - }, - { - "days": 4.00625, - "weight": 3.605946533647306e-05 - }, - { - "days": 4.007639, - "weight": 7.047986406674279e-05 - }, - { - "days": 4.008333, - "weight": 3.278133212406641e-05 - }, - { - "days": 4.011806, - "weight": 0.00013112532849626565 - }, - { - "days": 4.015278, - "weight": 6.392359764192951e-05 - }, - { - "days": 4.018056, - "weight": 5.57282646109129e-05 - }, - { - "days": 4.01875, - "weight": 6.392359764192951e-05 - }, - { - "days": 4.019444, - "weight": 0.00021963492523124496 - }, - { - "days": 4.020833, - "weight": 0.00011309559582802913 - }, - { - "days": 4.021528, - "weight": 4.5893864973692975e-05 - }, - { - "days": 4.025, - "weight": 0.0001753801268637553 - }, - { - "days": 4.029861, - "weight": 3.4420398730269734e-05 - }, - { - "days": 4.035417, - "weight": 3.7698531942676374e-05 - }, - { - "days": 4.047917, - "weight": 5.408919800470958e-05 - }, - { - "days": 4.054167, - "weight": 3.605946533647306e-05 - }, - { - "days": 4.059722, - "weight": 5.245013139850626e-05 - }, - { - "days": 4.063194, - "weight": 8.195333031016603e-05 - }, - { - "days": 4.069444, - "weight": 2.786413230545645e-05 - }, - { - "days": 4.070139, - "weight": 6.720173085433615e-05 - }, - { - "days": 4.074306, - "weight": 8.195333031016603e-05 - }, - { - "days": 4.078472, - "weight": 3.1142265517863095e-05 - }, - { - "days": 4.079861, - "weight": 3.9337598548879697e-05 - }, - { - "days": 4.080556, - "weight": 3.1142265517863095e-05 - }, - { - "days": 4.082639, - "weight": 0.00012292999546524905 - }, - { - "days": 4.095833, - "weight": 2.9503198911659772e-05 - }, - { - "days": 4.11875, - "weight": 3.1142265517863095e-05 - }, - { - "days": 4.127083, - "weight": 5.9006397823319545e-05 - }, - { - "days": 4.129167, - "weight": 3.9337598548879697e-05 - }, - { - "days": 4.131944, - "weight": 5.081106479230294e-05 - }, - { - "days": 4.1375, - "weight": 4.097666515508301e-05 - }, - { - "days": 4.148611, - "weight": 8.523146352257267e-05 - }, - { - "days": 4.181944, - "weight": 4.75329315798963e-05 - }, - { - "days": 4.209028, - "weight": 3.1142265517863095e-05 - }, - { - "days": 4.211806, - "weight": 3.4420398730269734e-05 - }, - { - "days": 4.23125, - "weight": 4.917199818609962e-05 - }, - { - "days": 4.268056, - "weight": 2.9503198911659772e-05 - }, - { - "days": 4.273611, - "weight": 3.278133212406641e-05 - }, - { - "days": 4.305556, - "weight": 2.9503198911659772e-05 - }, - { - "days": 4.334722, - "weight": 9.670492976599591e-05 - }, - { - "days": 4.438889, - "weight": 3.605946533647306e-05 - }, - { - "days": 4.450694, - "weight": 2.786413230545645e-05 - }, - { - "days": 4.467361, - "weight": 3.278133212406641e-05 - }, - { - "days": 4.619444, - "weight": 4.917199818609962e-05 - }, - { - "days": 4.731944, - "weight": 3.1142265517863095e-05 - }, - { - "days": 4.760417, - "weight": 3.278133212406641e-05 - }, - { - "days": 4.770833, - "weight": 0.00020160519256300844 - }, - { - "days": 4.801389, - "weight": 3.1142265517863095e-05 - }, - { - "days": 4.827083, - "weight": 0.00015407226098311215 - }, - { - "days": 4.884722, - "weight": 0.0002622506569925313 - }, - { - "days": 4.886806, - "weight": 2.9503198911659772e-05 - }, - { - "days": 4.900694, - "weight": 3.605946533647306e-05 - }, - { - "days": 4.940278, - "weight": 2.9503198911659772e-05 - }, - { - "days": 4.942361, - "weight": 6.884079746053947e-05 - }, - { - "days": 4.95, - "weight": 9.998306297840256e-05 - }, - { - "days": 4.951389, - "weight": 0.0002622506569925313 - }, - { - "days": 4.952083, - "weight": 2.9503198911659772e-05 - }, - { - "days": 4.952778, - "weight": 2.9503198911659772e-05 - }, - { - "days": 4.963889, - "weight": 4.2615731761286336e-05 - }, - { - "days": 4.970139, - "weight": 0.000168823860438942 - }, - { - "days": 4.978472, - "weight": 3.605946533647306e-05 - }, - { - "days": 4.9875, - "weight": 6.720173085433615e-05 - }, - { - "days": 4.99375, - "weight": 3.278133212406641e-05 - }, - { - "days": 5.0, - "weight": 2.786413230545645e-05 - }, - { - "days": 5.00625, - "weight": 3.4420398730269734e-05 - }, - { - "days": 5.007639, - "weight": 4.425479836748966e-05 - }, - { - "days": 5.011806, - "weight": 4.425479836748966e-05 - }, - { - "days": 5.016667, - "weight": 4.2615731761286336e-05 - }, - { - "days": 5.018056, - "weight": 0.00010653932940321584 - }, - { - "days": 5.019444, - "weight": 7.047986406674279e-05 - }, - { - "days": 5.027778, - "weight": 3.605946533647306e-05 - }, - { - "days": 5.03125, - "weight": 3.278133212406641e-05 - }, - { - "days": 5.054861, - "weight": 7.375799727914943e-05 - }, - { - "days": 5.078472, - "weight": 5.081106479230294e-05 - }, - { - "days": 5.086111, - "weight": 3.605946533647306e-05 - }, - { - "days": 5.095833, - "weight": 3.9337598548879697e-05 - }, - { - "days": 5.097917, - "weight": 2.9503198911659772e-05 - }, - { - "days": 5.103472, - "weight": 6.392359764192951e-05 - }, - { - "days": 5.114583, - "weight": 3.4420398730269734e-05 - }, - { - "days": 5.13125, - "weight": 3.278133212406641e-05 - }, - { - "days": 5.771528, - "weight": 6.392359764192951e-05 - }, - { - "days": 5.838194, - "weight": 9.50658631597926e-05 - }, - { - "days": 5.891667, - "weight": 3.9337598548879697e-05 - }, - { - "days": 5.91875, - "weight": 0.0001507941277707055 - }, - { - "days": 5.936111, - "weight": 4.917199818609962e-05 - }, - { - "days": 5.947917, - "weight": 3.1142265517863095e-05 - }, - { - "days": 5.986111, - "weight": 7.867519709775939e-05 - }, - { - "days": 5.997222, - "weight": 3.9337598548879697e-05 - }, - { - "days": 5.998611, - "weight": 3.278133212406641e-05 - }, - { - "days": 6.002778, - "weight": 4.917199818609962e-05 - }, - { - "days": 6.00625, - "weight": 4.425479836748966e-05 - }, - { - "days": 6.017361, - "weight": 4.097666515508301e-05 - }, - { - "days": 6.04375, - "weight": 5.736733121711622e-05 - }, - { - "days": 6.124306, - "weight": 3.9337598548879697e-05 - }, - { - "days": 6.765972, - "weight": 3.7698531942676374e-05 - }, - { - "days": 6.844444, - "weight": 6.884079746053947e-05 - }, - { - "days": 6.902083, - "weight": 7.047986406674279e-05 - }, - { - "days": 6.947917, - "weight": 3.278133212406641e-05 - }, - { - "days": 6.963194, - "weight": 7.375799727914943e-05 - }, - { - "days": 6.970833, - "weight": 3.605946533647306e-05 - }, - { - "days": 7.010417, - "weight": 5.736733121711622e-05 - }, - { - "days": 7.017361, - "weight": 4.75329315798963e-05 - }, - { - "days": 7.018056, - "weight": 2.9503198911659772e-05 - }, - { - "days": 7.051389, - "weight": 3.4420398730269734e-05 - }, - { - "days": 7.965278, - "weight": 3.278133212406641e-05 - }, - { - "days": 9.828472, - "weight": 9.014866334118263e-05 - }, - { - "days": 9.863889, - "weight": 4.75329315798963e-05 - }, - { - "days": 9.931944, - "weight": 9.342679655358928e-05 - }, - { - "days": 9.985417, - "weight": 4.097666515508301e-05 - }, - { - "days": 10.048611, - "weight": 3.7698531942676374e-05 - }, - { - "days": 10.845833, - "weight": 3.605946533647306e-05 - }, - { - "days": 10.914583, - "weight": 3.1142265517863095e-05 - }, - { - "days": 11.076389, - "weight": 6.392359764192951e-05 - }, - { - "days": 11.150694, - "weight": 3.1142265517863095e-05 - }, - { - "days": 11.209722, - "weight": 4.917199818609962e-05 - }, - { - "days": 11.26875, - "weight": 4.425479836748966e-05 - }, - { - "days": 11.844444, - "weight": 3.7698531942676374e-05 - }, - { - "days": 11.904861, - "weight": 3.605946533647306e-05 - }, - { - "days": 12.906944, - "weight": 3.9337598548879697e-05 - }, - { - "days": 16.880556, - "weight": 4.097666515508301e-05 - }, - { - "days": 17.958333, - "weight": 4.917199818609962e-05 - }, - { - "days": 22.287131, - "weight": 0.004166666666666667 - }, - { - "days": 25.1029, - "weight": 0.004166666666666667 - }, - { - "days": 28.274415, - "weight": 0.004166666666666667 - }, - { - "days": 31.846621, - "weight": 0.004166666666666667 - }, - { - "days": 35.870141, - "weight": 0.004166666666666667 - }, - { - "days": 40.401996, - "weight": 0.004166666666666667 - }, - { - "days": 45.506408, - "weight": 0.004166666666666667 - }, - { - "days": 51.255714, - "weight": 0.004166666666666667 - }, - { - "days": 57.73139, - "weight": 0.004166666666666667 - }, - { - "days": 65.025208, - "weight": 0.004166666666666667 - }, - { - "days": 73.240531, - "weight": 0.004166666666666667 - }, - { - "days": 82.493782, - "weight": 0.004166666666666667 - }, - { - "days": 92.916094, - "weight": 0.004166666666666667 - }, - { - "days": 104.655168, - "weight": 0.004166666666666667 - }, - { - "days": 117.877362, - "weight": 0.004166666666666667 - }, - { - "days": 132.770057, - "weight": 0.004166666666666667 - }, - { - "days": 149.544303, - "weight": 0.004166666666666667 - }, - { - "days": 168.437818, - "weight": 0.004166666666666667 - }, - { - "days": 189.71835, - "weight": 0.004166666666666667 - }, - { - "days": 213.687476, - "weight": 0.004166666666666667 - }, - { - "days": 240.684877, - "weight": 0.004166666666666667 - }, - { - "days": 271.093144, - "weight": 0.004166666666666667 - }, - { - "days": 305.34321, - "weight": 0.004166666666666667 - }, - { - "days": 343.920448, - "weight": 0.004166666666666667 - }, - { - "new_client": true, - "weight": 0.019885156066458687 - } - ] -} \ No newline at end of file diff --git a/tools/DeltaIndexTestTool/dist-tail-20.json b/tools/DeltaIndexTestTool/dist-tail-20.json deleted file mode 100644 index 670a029be0..0000000000 --- a/tools/DeltaIndexTestTool/dist-tail-20.json +++ /dev/null @@ -1,7301 +0,0 @@ -{ - "description": "Telemetry-derived from C:\\Users\\johnmcp\\Downloads\\export.csv: 686,366 download events, 1.77% net-new clients, observed ages 0-18.0 days, plus 20.0% reinstated stale tail out to 344 days", - "buckets": [ - { - "days": 0.0, - "weight": 4.079454664328265e-05 - }, - { - "days": 0.000694, - "weight": 0.00043271358403767666 - }, - { - "days": 0.028472, - "weight": 0.0017425099209059302 - }, - { - "days": 0.042361, - "weight": 4.6622339020894455e-05 - }, - { - "days": 0.043056, - "weight": 6.556266424813284e-05 - }, - { - "days": 0.047222, - "weight": 0.006537326099586045 - }, - { - "days": 0.049306, - "weight": 4.079454664328265e-05 - }, - { - "days": 0.050694, - "weight": 0.00283813488789695 - }, - { - "days": 0.051389, - "weight": 0.0070880524792703604 - }, - { - "days": 0.052083, - "weight": 2.9138961888059035e-05 - }, - { - "days": 0.054167, - "weight": 0.004095481093366698 - }, - { - "days": 0.054861, - "weight": 0.006413485511561794 - }, - { - "days": 0.055556, - "weight": 0.005271238205549879 - }, - { - "days": 0.056944, - "weight": 0.005603422371073753 - }, - { - "days": 0.057639, - "weight": 0.007732023536996465 - }, - { - "days": 0.058333, - "weight": 0.0034311127623189515 - }, - { - "days": 0.059028, - "weight": 0.03539946784970852 - }, - { - "days": 0.059722, - "weight": 0.016157554366928735 - }, - { - "days": 0.060417, - "weight": 0.006782093379445741 - }, - { - "days": 0.061111, - "weight": 0.014544712826424668 - }, - { - "days": 0.061806, - "weight": 0.016266825474008956 - }, - { - "days": 0.0625, - "weight": 0.0030348228806413484 - }, - { - "days": 0.063194, - "weight": 0.011224328119280341 - }, - { - "days": 0.063889, - "weight": 0.01250207159807173 - }, - { - "days": 0.064583, - "weight": 0.002959061579732395 - }, - { - "days": 0.065278, - "weight": 0.006581034542418133 - }, - { - "days": 0.065972, - "weight": 0.00822738588909347 - }, - { - "days": 0.066667, - "weight": 0.013556902018419466 - }, - { - "days": 0.067361, - "weight": 0.003750184394993198 - }, - { - "days": 0.069444, - "weight": 0.00525666872460585 - }, - { - "days": 0.070833, - "weight": 0.003211113600064106 - }, - { - "days": 0.072222, - "weight": 0.0033976029561476836 - }, - { - "days": 0.072917, - "weight": 0.0029809158011484395 - }, - { - "days": 0.074306, - "weight": 0.010382212120715434 - }, - { - "days": 0.076389, - "weight": 5.2450131398506267e-05 - }, - { - "days": 0.078472, - "weight": 0.004206209148541321 - }, - { - "days": 0.084028, - "weight": 0.0027915125488760556 - }, - { - "days": 0.084722, - "weight": 0.004050315702440206 - }, - { - "days": 0.085417, - "weight": 0.004124620055254756 - }, - { - "days": 0.0875, - "weight": 0.00019231714846118964 - }, - { - "days": 0.088889, - "weight": 0.0019406548617447318 - }, - { - "days": 0.102083, - "weight": 0.0010431748355925134 - }, - { - "days": 0.109722, - "weight": 0.000999466392760425 - }, - { - "days": 0.110417, - "weight": 0.0016099276443152617 - }, - { - "days": 0.113194, - "weight": 0.004019719792457744 - }, - { - "days": 0.114583, - "weight": 0.0012908560116410154 - }, - { - "days": 0.115278, - "weight": 5.827792377611807e-05 - }, - { - "days": 0.115972, - "weight": 3.350980617126789e-05 - }, - { - "days": 0.116667, - "weight": 0.0013083393887738507 - }, - { - "days": 0.117361, - "weight": 0.002609394037075687 - }, - { - "days": 0.118056, - "weight": 0.0026836983898902373 - }, - { - "days": 0.11875, - "weight": 0.0017279404399619008 - }, - { - "days": 0.119444, - "weight": 0.001360789520172357 - }, - { - "days": 0.120139, - "weight": 0.0017556224537555569 - }, - { - "days": 0.120833, - "weight": 0.0015312524472175023 - }, - { - "days": 0.121528, - "weight": 0.0015531066686335466 - }, - { - "days": 0.122222, - "weight": 0.0014132396515708632 - }, - { - "days": 0.122917, - "weight": 0.0011305917212566907 - }, - { - "days": 0.123611, - "weight": 0.0005419846911178981 - }, - { - "days": 0.124306, - "weight": 0.001602642903843247 - }, - { - "days": 0.125, - "weight": 0.0005958917706108072 - }, - { - "days": 0.125694, - "weight": 0.0037312440697659594 - }, - { - "days": 0.126389, - "weight": 0.0006876795005581933 - }, - { - "days": 0.127083, - "weight": 0.0022378722730029338 - }, - { - "days": 0.127778, - "weight": 0.0022553556501357696 - }, - { - "days": 0.130556, - "weight": 0.0002476811760485018 - }, - { - "days": 0.13125, - "weight": 0.0011728432159943763 - }, - { - "days": 0.131944, - "weight": 0.0006264876805932692 - }, - { - "days": 0.132639, - "weight": 0.001210723866448853 - }, - { - "days": 0.133333, - "weight": 0.0024083352000480794 - }, - { - "days": 0.134028, - "weight": 0.00187072135321339 - }, - { - "days": 0.134722, - "weight": 0.0005769514453835689 - }, - { - "days": 0.136111, - "weight": 0.0011903265931272116 - }, - { - "days": 0.1375, - "weight": 0.0004968193001914066 - }, - { - "days": 0.138194, - "weight": 0.0004385413764152885 - }, - { - "days": 0.14375, - "weight": 0.0003482105945623055 - }, - { - "days": 0.144444, - "weight": 0.001027148406554081 - }, - { - "days": 0.147917, - "weight": 0.0009441023651731127 - }, - { - "days": 0.148611, - "weight": 0.00021271442178283096 - }, - { - "days": 0.149306, - "weight": 0.0007998645038272206 - }, - { - "days": 0.150694, - "weight": 0.0010941680188966169 - }, - { - "days": 0.161111, - "weight": 0.0006177459920268516 - }, - { - "days": 0.164583, - "weight": 3.059590998246199e-05 - }, - { - "days": 0.168056, - "weight": 0.0006090043034604339 - }, - { - "days": 0.169444, - "weight": 0.0013680742606443717 - }, - { - "days": 0.172222, - "weight": 0.0006090043034604339 - }, - { - "days": 0.172917, - "weight": 5.3907079492909214e-05 - }, - { - "days": 0.173611, - "weight": 0.000904764766624233 - }, - { - "days": 0.175, - "weight": 0.0004603955978313328 - }, - { - "days": 0.175694, - "weight": 0.00112039308459587 - }, - { - "days": 0.176389, - "weight": 0.0005201304697018537 - }, - { - "days": 0.177083, - "weight": 0.0008945661299634124 - }, - { - "days": 0.177778, - "weight": 0.0016492652428641414 - }, - { - "days": 0.178472, - "weight": 0.0005376138468346892 - }, - { - "days": 0.179167, - "weight": 0.0019523104464999554 - }, - { - "days": 0.179861, - "weight": 0.0008989369742466212 - }, - { - "days": 0.18125, - "weight": 0.0021271442178283097 - }, - { - "days": 0.181944, - "weight": 0.0011568167869559437 - }, - { - "days": 0.182639, - "weight": 0.0004676803383033475 - }, - { - "days": 0.183333, - "weight": 0.00046622339020894457 - }, - { - "days": 0.184028, - "weight": 0.0008843674933025918 - }, - { - "days": 0.184722, - "weight": 0.0012019821778824352 - }, - { - "days": 0.185417, - "weight": 0.0006148320958380457 - }, - { - "days": 0.186111, - "weight": 0.002207276363020472 - }, - { - "days": 0.186806, - "weight": 0.00026516455318133724 - }, - { - "days": 0.1875, - "weight": 0.00038026345263917044 - }, - { - "days": 0.188889, - "weight": 0.0006993350853134169 - }, - { - "days": 0.190972, - "weight": 0.0004283427397544678 - }, - { - "days": 0.192361, - "weight": 0.0012500614649977326 - }, - { - "days": 0.194444, - "weight": 0.0005652958606283453 - }, - { - "days": 0.195139, - "weight": 0.0024505866947857648 - }, - { - "days": 0.196528, - "weight": 0.0010111219775156485 - }, - { - "days": 0.197222, - "weight": 0.00035403838693991727 - }, - { - "days": 0.197917, - "weight": 0.0002301977989156664 - }, - { - "days": 0.2, - "weight": 0.0003161577364854405 - }, - { - "days": 0.202083, - "weight": 0.0012893990635466123 - }, - { - "days": 0.204167, - "weight": 0.0005405277430234951 - }, - { - "days": 0.208333, - "weight": 0.0005798653415723748 - }, - { - "days": 0.210417, - "weight": 0.0007066198257854316 - }, - { - "days": 0.211111, - "weight": 0.00043271358403767666 - }, - { - "days": 0.2125, - "weight": 0.0005259582620794656 - }, - { - "days": 0.218056, - "weight": 0.00018066156370596603 - }, - { - "days": 0.222917, - "weight": 0.00029138961888059035 - }, - { - "days": 0.227083, - "weight": 0.0002797340341253667 - }, - { - "days": 0.229167, - "weight": 9.470162613619187e-05 - }, - { - "days": 0.23125, - "weight": 0.0006410571615372988 - }, - { - "days": 0.232639, - "weight": 0.00010635721089141548 - }, - { - "days": 0.233333, - "weight": 8.74168856641771e-05 - }, - { - "days": 0.234722, - "weight": 9.615857423059482e-05 - }, - { - "days": 0.235417, - "weight": 9.324467804178891e-05 - }, - { - "days": 0.236111, - "weight": 0.00024622422795409887 - }, - { - "days": 0.2375, - "weight": 0.00024331033176529295 - }, - { - "days": 0.238194, - "weight": 0.00010052941851380368 - }, - { - "days": 0.238889, - "weight": 0.00038026345263917044 - }, - { - "days": 0.239583, - "weight": 0.0002025157851220103 - }, - { - "days": 0.240278, - "weight": 0.0003715217640727527 - }, - { - "days": 0.240972, - "weight": 0.0003948329335831999 - }, - { - "days": 0.242361, - "weight": 0.00032489942505185826 - }, - { - "days": 0.243056, - "weight": 0.0007211893067294612 - }, - { - "days": 0.24375, - "weight": 0.0004968193001914066 - }, - { - "days": 0.244444, - "weight": 0.00010198636660820662 - }, - { - "days": 0.245833, - "weight": 0.00021562831797163686 - }, - { - "days": 0.247917, - "weight": 0.000272449293653352 - }, - { - "days": 0.248611, - "weight": 7.284740472014759e-05 - }, - { - "days": 0.249306, - "weight": 0.00022291305844365162 - }, - { - "days": 0.251389, - "weight": 0.00011364195136343023 - }, - { - "days": 0.252083, - "weight": 0.0010548304203477372 - }, - { - "days": 0.252778, - "weight": 0.00024622422795409887 - }, - { - "days": 0.253472, - "weight": 0.0004166871549992442 - }, - { - "days": 0.254167, - "weight": 0.00020980052559402507 - }, - { - "days": 0.254861, - "weight": 9.907247041940072e-05 - }, - { - "days": 0.255556, - "weight": 0.00011946974374104205 - }, - { - "days": 0.25625, - "weight": 0.00010781415898581843 - }, - { - "days": 0.258333, - "weight": 0.00023311169510447228 - }, - { - "days": 0.259028, - "weight": 7.867519709775939e-05 - }, - { - "days": 0.259722, - "weight": 6.993350853134168e-05 - }, - { - "days": 0.261111, - "weight": 0.00022582695463245752 - }, - { - "days": 0.265278, - "weight": 0.00013403922468507157 - }, - { - "days": 0.268056, - "weight": 0.0001777476675171601 - }, - { - "days": 0.271528, - "weight": 0.0003992037778664088 - }, - { - "days": 0.274306, - "weight": 9.470162613619187e-05 - }, - { - "days": 0.275, - "weight": 0.00010198636660820662 - }, - { - "days": 0.276389, - "weight": 0.0001150988994578332 - }, - { - "days": 0.279861, - "weight": 0.0002025157851220103 - }, - { - "days": 0.281944, - "weight": 0.00014278091325148928 - }, - { - "days": 0.284028, - "weight": 3.788065045447675e-05 - }, - { - "days": 0.286806, - "weight": 0.00011655584755223614 - }, - { - "days": 0.288194, - "weight": 7.867519709775939e-05 - }, - { - "days": 0.290278, - "weight": 0.00021271442178283096 - }, - { - "days": 0.292361, - "weight": 9.761552232499777e-05 - }, - { - "days": 0.293056, - "weight": 0.0001267544842130568 - }, - { - "days": 0.295139, - "weight": 0.00011655584755223614 - }, - { - "days": 0.297222, - "weight": 6.993350853134168e-05 - }, - { - "days": 0.297917, - "weight": 0.0003511244907511114 - }, - { - "days": 0.299306, - "weight": 0.00039628988167760286 - }, - { - "days": 0.3, - "weight": 0.00021562831797163686 - }, - { - "days": 0.300694, - "weight": 0.00029721741125820215 - }, - { - "days": 0.301389, - "weight": 0.00018357545989477193 - }, - { - "days": 0.303472, - "weight": 6.556266424813284e-05 - }, - { - "days": 0.304167, - "weight": 0.00030741604791902283 - }, - { - "days": 0.305556, - "weight": 0.0003161577364854405 - }, - { - "days": 0.30625, - "weight": 0.00017629071942275716 - }, - { - "days": 0.306944, - "weight": 0.00013986701706268336 - }, - { - "days": 0.308333, - "weight": 0.00024185338367089 - }, - { - "days": 0.309722, - "weight": 0.0010184067179876634 - }, - { - "days": 0.310417, - "weight": 0.0001150988994578332 - }, - { - "days": 0.311111, - "weight": 0.00016172123847872766 - }, - { - "days": 0.311806, - "weight": 0.0003875481931111852 - }, - { - "days": 0.3125, - "weight": 0.0002447672798596959 - }, - { - "days": 0.313194, - "weight": 0.00035695228312872316 - }, - { - "days": 0.313889, - "weight": 0.00011072805517462434 - }, - { - "days": 0.314583, - "weight": 0.00015006565372350402 - }, - { - "days": 0.315972, - "weight": 0.00011218500326902729 - }, - { - "days": 0.316667, - "weight": 8.74168856641771e-05 - }, - { - "days": 0.317361, - "weight": 0.00010781415898581843 - }, - { - "days": 0.31875, - "weight": 4.22514947376856e-05 - }, - { - "days": 0.320139, - "weight": 0.00030013130744700804 - }, - { - "days": 0.320833, - "weight": 8.013214519216235e-05 - }, - { - "days": 0.322222, - "weight": 0.00016463513466753355 - }, - { - "days": 0.323611, - "weight": 4.370844283208855e-05 - }, - { - "days": 0.326389, - "weight": 0.000151522601817907 - }, - { - "days": 0.327778, - "weight": 0.00012238363992984796 - }, - { - "days": 0.33125, - "weight": 0.00017920461561156306 - }, - { - "days": 0.332639, - "weight": 0.006036135955111429 - }, - { - "days": 0.335417, - "weight": 0.00016754903085633945 - }, - { - "days": 0.338194, - "weight": 8.45029894753712e-05 - }, - { - "days": 0.340278, - "weight": 0.0001384100689682804 - }, - { - "days": 0.340972, - "weight": 0.00030158825554141104 - }, - { - "days": 0.343056, - "weight": 0.00011655584755223614 - }, - { - "days": 0.34375, - "weight": 0.00014715175753469813 - }, - { - "days": 0.345139, - "weight": 2.7682013793656084e-05 - }, - { - "days": 0.345833, - "weight": 0.00013986701706268336 - }, - { - "days": 0.349306, - "weight": 9.470162613619187e-05 - }, - { - "days": 0.353472, - "weight": 0.00011946974374104205 - }, - { - "days": 0.354861, - "weight": 0.00010927110708022139 - }, - { - "days": 0.356944, - "weight": 6.847656043693873e-05 - }, - { - "days": 0.357639, - "weight": 0.00013695312087387746 - }, - { - "days": 0.358333, - "weight": 5.9734871870521025e-05 - }, - { - "days": 0.359028, - "weight": 0.0005361568987402863 - }, - { - "days": 0.359722, - "weight": 0.00021999916225484573 - }, - { - "days": 0.360417, - "weight": 0.0001748337713283542 - }, - { - "days": 0.361111, - "weight": 0.00010198636660820662 - }, - { - "days": 0.361806, - "weight": 4.370844283208855e-05 - }, - { - "days": 0.3625, - "weight": 0.00027390624174775493 - }, - { - "days": 0.363194, - "weight": 0.0002054296813108162 - }, - { - "days": 0.363889, - "weight": 8.15890932865653e-05 - }, - { - "days": 0.365972, - "weight": 0.00017191987513954832 - }, - { - "days": 0.366667, - "weight": 8.304604138096825e-05 - }, - { - "days": 0.367361, - "weight": 6.119181996492398e-05 - }, - { - "days": 0.36875, - "weight": 0.00010198636660820662 - }, - { - "days": 0.369444, - "weight": 0.0001267544842130568 - }, - { - "days": 0.370139, - "weight": 0.00024185338367089 - }, - { - "days": 0.371528, - "weight": 0.0003788065045447675 - }, - { - "days": 0.372222, - "weight": 0.00014423786134589223 - }, - { - "days": 0.372917, - "weight": 0.00020105883702760735 - }, - { - "days": 0.373611, - "weight": 0.00017920461561156306 - }, - { - "days": 0.375, - "weight": 0.0014656897829693695 - }, - { - "days": 0.376389, - "weight": 0.0003452966983734996 - }, - { - "days": 0.377083, - "weight": 0.0001384100689682804 - }, - { - "days": 0.377778, - "weight": 0.0001631781865731306 - }, - { - "days": 0.378472, - "weight": 0.00014423786134589223 - }, - { - "days": 0.379861, - "weight": 0.00011655584755223614 - }, - { - "days": 0.38125, - "weight": 0.00012238363992984796 - }, - { - "days": 0.3875, - "weight": 0.0001150988994578332 - }, - { - "days": 0.388889, - "weight": 4.516539092649151e-05 - }, - { - "days": 0.390278, - "weight": 0.00010344331470260957 - }, - { - "days": 0.392361, - "weight": 0.00010344331470260957 - }, - { - "days": 0.397222, - "weight": 0.00018794630417798077 - }, - { - "days": 0.398611, - "weight": 0.00021271442178283096 - }, - { - "days": 0.399306, - "weight": 0.00011655584755223614 - }, - { - "days": 0.400694, - "weight": 0.0001908602003667867 - }, - { - "days": 0.404167, - "weight": 0.00011801279564663909 - }, - { - "days": 0.406944, - "weight": 0.001660920827619365 - }, - { - "days": 0.407639, - "weight": 0.00014569480944029518 - }, - { - "days": 0.409028, - "weight": 0.00011218500326902729 - }, - { - "days": 0.4125, - "weight": 0.00023602559129327818 - }, - { - "days": 0.414583, - "weight": 6.410571615372988e-05 - }, - { - "days": 0.415972, - "weight": 7.867519709775939e-05 - }, - { - "days": 0.418056, - "weight": 0.00024331033176529295 - }, - { - "days": 0.41875, - "weight": 9.615857423059482e-05 - }, - { - "days": 0.419444, - "weight": 0.0001908602003667867 - }, - { - "days": 0.420139, - "weight": 0.0002768201379365608 - }, - { - "days": 0.421528, - "weight": 0.0003788065045447675 - }, - { - "days": 0.422222, - "weight": 0.00018940325227238374 - }, - { - "days": 0.422917, - "weight": 7.721824900335645e-05 - }, - { - "days": 0.424306, - "weight": 0.00019523104464999554 - }, - { - "days": 0.425, - "weight": 0.0002753631898421579 - }, - { - "days": 0.426389, - "weight": 0.00010927110708022139 - }, - { - "days": 0.427083, - "weight": 0.0001908602003667867 - }, - { - "days": 0.427778, - "weight": 0.00024331033176529295 - }, - { - "days": 0.429861, - "weight": 0.0014074118591932514 - }, - { - "days": 0.43125, - "weight": 4.370844283208855e-05 - }, - { - "days": 0.431944, - "weight": 0.00015880734228992173 - }, - { - "days": 0.432639, - "weight": 0.0001937740965555926 - }, - { - "days": 0.434028, - "weight": 0.00017046292704514537 - }, - { - "days": 0.435417, - "weight": 0.00011801279564663909 - }, - { - "days": 0.436806, - "weight": 3.9337598548879697e-05 - }, - { - "days": 0.4375, - "weight": 0.0006614544348589401 - }, - { - "days": 0.438194, - "weight": 0.00019668799274439848 - }, - { - "days": 0.438889, - "weight": 0.00024185338367089 - }, - { - "days": 0.440278, - "weight": 0.00013112532849626567 - }, - { - "days": 0.442361, - "weight": 0.00015006565372350402 - }, - { - "days": 0.443056, - "weight": 0.0001296683804018627 - }, - { - "days": 0.446528, - "weight": 0.00010490026279701253 - }, - { - "days": 0.447222, - "weight": 0.0002986743593526051 - }, - { - "days": 0.447917, - "weight": 0.00018211851180036898 - }, - { - "days": 0.449306, - "weight": 0.00013695312087387746 - }, - { - "days": 0.453472, - "weight": 0.000332184165523873 - }, - { - "days": 0.457639, - "weight": 0.0003205285807686494 - }, - { - "days": 0.458333, - "weight": 0.0003875481931111852 - }, - { - "days": 0.459028, - "weight": 6.993350853134168e-05 - }, - { - "days": 0.460417, - "weight": 0.00019523104464999554 - }, - { - "days": 0.4625, - "weight": 0.0008683410642641592 - }, - { - "days": 0.466667, - "weight": 0.0017643641423219746 - }, - { - "days": 0.468056, - "weight": 0.0003817204007335734 - }, - { - "days": 0.470833, - "weight": 4.079454664328265e-05 - }, - { - "days": 0.472222, - "weight": 0.00017191987513954832 - }, - { - "days": 0.472917, - "weight": 0.00015589344610111584 - }, - { - "days": 0.473611, - "weight": 0.0004283427397544678 - }, - { - "days": 0.474306, - "weight": 0.0004647664421145416 - }, - { - "days": 0.475, - "weight": 0.0002535089684261136 - }, - { - "days": 0.477083, - "weight": 0.0008421159985649062 - }, - { - "days": 0.477778, - "weight": 0.00028264793031417267 - }, - { - "days": 0.479167, - "weight": 0.00040066072596081176 - }, - { - "days": 0.480556, - "weight": 0.0004385413764152885 - }, - { - "days": 0.48125, - "weight": 0.00048224981924737705 - }, - { - "days": 0.483333, - "weight": 0.0003773495564503645 - }, - { - "days": 0.484028, - "weight": 0.0011699293198055704 - }, - { - "days": 0.484722, - "weight": 0.00026225065699253135 - }, - { - "days": 0.485417, - "weight": 0.0002535089684261136 - }, - { - "days": 0.486806, - "weight": 0.00105628736844214 - }, - { - "days": 0.4875, - "weight": 0.00047496507877536225 - }, - { - "days": 0.488194, - "weight": 0.00030158825554141104 - }, - { - "days": 0.488889, - "weight": 0.00026516455318133724 - }, - { - "days": 0.490278, - "weight": 0.0006935072929358051 - }, - { - "days": 0.490972, - "weight": 0.0002025157851220103 - }, - { - "days": 0.491667, - "weight": 0.00035403838693991727 - }, - { - "days": 0.492361, - "weight": 0.00022582695463245752 - }, - { - "days": 0.493056, - "weight": 0.0006935072929358051 - }, - { - "days": 0.49375, - "weight": 0.0005609250163451364 - }, - { - "days": 0.494444, - "weight": 0.00307998827156784 - }, - { - "days": 0.495833, - "weight": 0.00033946890599588774 - }, - { - "days": 0.498611, - "weight": 0.0006556266424813283 - }, - { - "days": 0.499306, - "weight": 0.00023165474701006934 - }, - { - "days": 0.500694, - "weight": 0.00025787981270932245 - }, - { - "days": 0.502083, - "weight": 0.00028264793031417267 - }, - { - "days": 0.50625, - "weight": 0.00032489942505185826 - }, - { - "days": 0.506944, - "weight": 0.000665825279142149 - }, - { - "days": 0.509028, - "weight": 0.00032489942505185826 - }, - { - "days": 0.511111, - "weight": 0.0009703274308723659 - }, - { - "days": 0.5125, - "weight": 0.00012238363992984796 - }, - { - "days": 0.517361, - "weight": 0.00039628988167760286 - }, - { - "days": 0.51875, - "weight": 0.00023311169510447228 - }, - { - "days": 0.521528, - "weight": 0.0004807928711529741 - }, - { - "days": 0.522917, - "weight": 0.00023748253938768113 - }, - { - "days": 0.524306, - "weight": 0.00034383975027909664 - }, - { - "days": 0.525, - "weight": 0.0001602642903843247 - }, - { - "days": 0.526389, - "weight": 0.00022291305844365162 - }, - { - "days": 0.527083, - "weight": 0.00016172123847872766 - }, - { - "days": 0.532639, - "weight": 0.0003584092312231261 - }, - { - "days": 0.533333, - "weight": 0.0006439710577261047 - }, - { - "days": 0.535417, - "weight": 0.00039628988167760286 - }, - { - "days": 0.536111, - "weight": 0.00027099234555894903 - }, - { - "days": 0.536806, - "weight": 0.0011990682816936293 - }, - { - "days": 0.538194, - "weight": 7.57613009089535e-05 - }, - { - "days": 0.539583, - "weight": 0.0005041040406634213 - }, - { - "days": 0.540278, - "weight": 0.0004807928711529741 - }, - { - "days": 0.543056, - "weight": 0.0004079454664328265 - }, - { - "days": 0.544444, - "weight": 0.000754699112900729 - }, - { - "days": 0.545833, - "weight": 0.0003263563731462612 - }, - { - "days": 0.546528, - "weight": 0.0006279446286876722 - }, - { - "days": 0.547222, - "weight": 0.0002841048784085756 - }, - { - "days": 0.548611, - "weight": 0.00017920461561156306 - }, - { - "days": 0.549306, - "weight": 0.00026807844937014314 - }, - { - "days": 0.55, - "weight": 0.0012762865306969857 - }, - { - "days": 0.550694, - "weight": 0.000847943790942518 - }, - { - "days": 0.551389, - "weight": 0.00036715091978954385 - }, - { - "days": 0.552083, - "weight": 8.595993756977416e-05 - }, - { - "days": 0.552778, - "weight": 0.00017337682323395127 - }, - { - "days": 0.553472, - "weight": 0.0004501969611705121 - }, - { - "days": 0.554167, - "weight": 5.3907079492909214e-05 - }, - { - "days": 0.554861, - "weight": 0.00023165474701006934 - }, - { - "days": 0.555556, - "weight": 0.0002054296813108162 - }, - { - "days": 0.556944, - "weight": 0.0001544364980067129 - }, - { - "days": 0.557639, - "weight": 0.0002593367608037254 - }, - { - "days": 0.558333, - "weight": 0.0002870187745973815 - }, - { - "days": 0.559722, - "weight": 0.0003525814388455143 - }, - { - "days": 0.560417, - "weight": 9.178772994738596e-05 - }, - { - "days": 0.568056, - "weight": 0.000120926691835445 - }, - { - "days": 0.570139, - "weight": 0.00046913728639775046 - }, - { - "days": 0.570833, - "weight": 7.57613009089535e-05 - }, - { - "days": 0.572917, - "weight": 0.00013112532849626567 - }, - { - "days": 0.575, - "weight": 0.00014715175753469813 - }, - { - "days": 0.58125, - "weight": 0.00020688662940521914 - }, - { - "days": 0.584722, - "weight": 0.0005580111201563305 - }, - { - "days": 0.5875, - "weight": 3.205285807686494e-05 - }, - { - "days": 0.590278, - "weight": 6.410571615372988e-05 - }, - { - "days": 0.591667, - "weight": 0.0003234424769574553 - }, - { - "days": 0.592361, - "weight": 0.00018503240798917487 - }, - { - "days": 0.593056, - "weight": 7.430435281455054e-05 - }, - { - "days": 0.59375, - "weight": 9.615857423059482e-05 - }, - { - "days": 0.595833, - "weight": 0.00010344331470260957 - }, - { - "days": 0.597222, - "weight": 0.00021854221416044278 - }, - { - "days": 0.597917, - "weight": 0.0009688704827779629 - }, - { - "days": 0.598611, - "weight": 0.0003423828021846937 - }, - { - "days": 0.599306, - "weight": 7.721824900335645e-05 - }, - { - "days": 0.6, - "weight": 0.00011072805517462434 - }, - { - "days": 0.601389, - "weight": 0.00024185338367089 - }, - { - "days": 0.602083, - "weight": 4.953623520970036e-05 - }, - { - "days": 0.602778, - "weight": 0.0001748337713283542 - }, - { - "days": 0.603472, - "weight": 0.00018211851180036898 - }, - { - "days": 0.605556, - "weight": 0.0003875481931111852 - }, - { - "days": 0.606944, - "weight": 0.00014569480944029518 - }, - { - "days": 0.607639, - "weight": 0.0001267544842130568 - }, - { - "days": 0.609028, - "weight": 0.00013258227659066862 - }, - { - "days": 0.609722, - "weight": 0.0002083435774996221 - }, - { - "days": 0.610417, - "weight": 0.00011072805517462434 - }, - { - "days": 0.611111, - "weight": 0.00151522601817907 - }, - { - "days": 0.611806, - "weight": 0.00026516455318133724 - }, - { - "days": 0.6125, - "weight": 5.6820975681715117e-05 - }, - { - "days": 0.613194, - "weight": 4.370844283208855e-05 - }, - { - "days": 0.613889, - "weight": 0.00025205202033171066 - }, - { - "days": 0.614583, - "weight": 0.00022874085082126344 - }, - { - "days": 0.615278, - "weight": 6.410571615372988e-05 - }, - { - "days": 0.615972, - "weight": 6.410571615372988e-05 - }, - { - "days": 0.616667, - "weight": 0.0002986743593526051 - }, - { - "days": 0.617361, - "weight": 0.00018357545989477193 - }, - { - "days": 0.61875, - "weight": 0.00015880734228992173 - }, - { - "days": 0.619444, - "weight": 7.284740472014759e-05 - }, - { - "days": 0.620139, - "weight": 0.0002637076050869343 - }, - { - "days": 0.622222, - "weight": 6.847656043693873e-05 - }, - { - "days": 0.623611, - "weight": 8.304604138096825e-05 - }, - { - "days": 0.624306, - "weight": 9.615857423059482e-05 - }, - { - "days": 0.628472, - "weight": 6.847656043693873e-05 - }, - { - "days": 0.631944, - "weight": 0.00021854221416044278 - }, - { - "days": 0.632639, - "weight": 6.993350853134168e-05 - }, - { - "days": 0.634028, - "weight": 0.00036423702360073796 - }, - { - "days": 0.642361, - "weight": 9.178772994738596e-05 - }, - { - "days": 0.644444, - "weight": 0.00044291222069849735 - }, - { - "days": 0.648611, - "weight": 9.470162613619187e-05 - }, - { - "days": 0.652778, - "weight": 0.0013374783506619097 - }, - { - "days": 0.654167, - "weight": 0.0003744356602615586 - }, - { - "days": 0.654861, - "weight": 0.00011364195136343023 - }, - { - "days": 0.655556, - "weight": 0.0003234424769574553 - }, - { - "days": 0.65625, - "weight": 4.807928711529741e-05 - }, - { - "days": 0.656944, - "weight": 6.556266424813284e-05 - }, - { - "days": 0.657639, - "weight": 4.22514947376856e-05 - }, - { - "days": 0.658333, - "weight": 0.00017337682323395127 - }, - { - "days": 0.659028, - "weight": 5.9734871870521025e-05 - }, - { - "days": 0.660417, - "weight": 0.00019231714846118964 - }, - { - "days": 0.661806, - "weight": 0.00014423786134589223 - }, - { - "days": 0.6625, - "weight": 6.119181996492398e-05 - }, - { - "days": 0.663194, - "weight": 8.887383375858005e-05 - }, - { - "days": 0.663889, - "weight": 4.807928711529741e-05 - }, - { - "days": 0.664583, - "weight": 0.0007139045662574463 - }, - { - "days": 0.665278, - "weight": 0.00010344331470260957 - }, - { - "days": 0.665972, - "weight": 0.00014423786134589223 - }, - { - "days": 0.666667, - "weight": 9.324467804178891e-05 - }, - { - "days": 0.667361, - "weight": 0.0001267544842130568 - }, - { - "days": 0.668056, - "weight": 3.350980617126789e-05 - }, - { - "days": 0.66875, - "weight": 0.0008377451542816973 - }, - { - "days": 0.669444, - "weight": 0.00017337682323395127 - }, - { - "days": 0.670139, - "weight": 0.001354961727794745 - }, - { - "days": 0.670833, - "weight": 0.0001267544842130568 - }, - { - "days": 0.671528, - "weight": 0.00011364195136343023 - }, - { - "days": 0.672917, - "weight": 0.00019814494083880143 - }, - { - "days": 0.673611, - "weight": 0.00017191987513954832 - }, - { - "days": 0.674306, - "weight": 0.00013549617277947452 - }, - { - "days": 0.675, - "weight": 6.264876805932693e-05 - }, - { - "days": 0.675694, - "weight": 0.00020105883702760735 - }, - { - "days": 0.676389, - "weight": 0.00015589344610111584 - }, - { - "days": 0.677083, - "weight": 0.00018648935608357782 - }, - { - "days": 0.678472, - "weight": 8.013214519216235e-05 - }, - { - "days": 0.68125, - "weight": 4.807928711529741e-05 - }, - { - "days": 0.681944, - "weight": 0.00022582695463245752 - }, - { - "days": 0.682639, - "weight": 0.00010198636660820662 - }, - { - "days": 0.683333, - "weight": 0.0003132438402966346 - }, - { - "days": 0.684028, - "weight": 0.00036132312741193206 - }, - { - "days": 0.6875, - "weight": 0.00010781415898581843 - }, - { - "days": 0.688889, - "weight": 0.00014423786134589223 - }, - { - "days": 0.690278, - "weight": 4.516539092649151e-05 - }, - { - "days": 0.69375, - "weight": 8.304604138096825e-05 - }, - { - "days": 0.695833, - "weight": 5.536402758731217e-05 - }, - { - "days": 0.698611, - "weight": 0.0007241032029182671 - }, - { - "days": 0.70625, - "weight": 2.6225065699253133e-05 - }, - { - "days": 0.707639, - "weight": 9.178772994738596e-05 - }, - { - "days": 0.713889, - "weight": 0.0003496675426567084 - }, - { - "days": 0.716667, - "weight": 0.0001296683804018627 - }, - { - "days": 0.71875, - "weight": 0.0006774808638973725 - }, - { - "days": 0.719444, - "weight": 0.0006235737844044634 - }, - { - "days": 0.720139, - "weight": 0.0012296641916760913 - }, - { - "days": 0.720833, - "weight": 0.0003132438402966346 - }, - { - "days": 0.721528, - "weight": 0.00016463513466753355 - }, - { - "days": 0.722222, - "weight": 7.867519709775939e-05 - }, - { - "days": 0.723611, - "weight": 0.00017191987513954832 - }, - { - "days": 0.725, - "weight": 7.284740472014759e-05 - }, - { - "days": 0.725694, - "weight": 9.761552232499777e-05 - }, - { - "days": 0.726389, - "weight": 0.000303045203635814 - }, - { - "days": 0.727083, - "weight": 9.178772994738596e-05 - }, - { - "days": 0.727778, - "weight": 0.00012529753611865386 - }, - { - "days": 0.728472, - "weight": 0.00019231714846118964 - }, - { - "days": 0.729167, - "weight": 0.00024913812414290476 - }, - { - "days": 0.729861, - "weight": 9.033078185298302e-05 - }, - { - "days": 0.730556, - "weight": 0.0010999958112742287 - }, - { - "days": 0.73125, - "weight": 8.013214519216235e-05 - }, - { - "days": 0.731944, - "weight": 6.119181996492398e-05 - }, - { - "days": 0.732639, - "weight": 0.00011218500326902729 - }, - { - "days": 0.733333, - "weight": 6.556266424813284e-05 - }, - { - "days": 0.734028, - "weight": 0.00020980052559402507 - }, - { - "days": 0.734722, - "weight": 7.430435281455054e-05 - }, - { - "days": 0.735417, - "weight": 0.00028556182650297856 - }, - { - "days": 0.736806, - "weight": 8.45029894753712e-05 - }, - { - "days": 0.7375, - "weight": 0.00012238363992984796 - }, - { - "days": 0.738194, - "weight": 0.0005725806011003601 - }, - { - "days": 0.738889, - "weight": 0.00022145611034924868 - }, - { - "days": 0.740278, - "weight": 0.00011801279564663909 - }, - { - "days": 0.740972, - "weight": 5.099318330410331e-05 - }, - { - "days": 0.741667, - "weight": 9.033078185298302e-05 - }, - { - "days": 0.743056, - "weight": 3.496675426567084e-05 - }, - { - "days": 0.744444, - "weight": 8.595993756977416e-05 - }, - { - "days": 0.746528, - "weight": 8.304604138096825e-05 - }, - { - "days": 0.747222, - "weight": 0.0004501969611705121 - }, - { - "days": 0.748611, - "weight": 0.00010927110708022139 - }, - { - "days": 0.749306, - "weight": 0.000120926691835445 - }, - { - "days": 0.75, - "weight": 5.099318330410331e-05 - }, - { - "days": 0.750694, - "weight": 9.761552232499777e-05 - }, - { - "days": 0.7625, - "weight": 5.827792377611807e-05 - }, - { - "days": 0.776389, - "weight": 0.0001908602003667867 - }, - { - "days": 0.777083, - "weight": 5.2450131398506267e-05 - }, - { - "days": 0.777778, - "weight": 4.807928711529741e-05 - }, - { - "days": 0.778472, - "weight": 0.000423971895471259 - }, - { - "days": 0.779167, - "weight": 0.00016172123847872766 - }, - { - "days": 0.779861, - "weight": 0.002017873110748088 - }, - { - "days": 0.780556, - "weight": 0.00037589260835596154 - }, - { - "days": 0.78125, - "weight": 9.178772994738596e-05 - }, - { - "days": 0.781944, - "weight": 8.15890932865653e-05 - }, - { - "days": 0.782639, - "weight": 0.00027827708603096377 - }, - { - "days": 0.783333, - "weight": 7.867519709775939e-05 - }, - { - "days": 0.784028, - "weight": 0.00017191987513954832 - }, - { - "days": 0.785417, - "weight": 0.0003788065045447675 - }, - { - "days": 0.786806, - "weight": 0.0002535089684261136 - }, - { - "days": 0.7875, - "weight": 0.0023194613662894993 - }, - { - "days": 0.788194, - "weight": 0.0004939054040026007 - }, - { - "days": 0.788889, - "weight": 3.788065045447675e-05 - }, - { - "days": 0.789583, - "weight": 0.0006541696943869254 - }, - { - "days": 0.790972, - "weight": 6.119181996492398e-05 - }, - { - "days": 0.791667, - "weight": 0.00029138961888059035 - }, - { - "days": 0.792361, - "weight": 0.0002564228646149195 - }, - { - "days": 0.793056, - "weight": 8.15890932865653e-05 - }, - { - "days": 0.79375, - "weight": 0.00011364195136343023 - }, - { - "days": 0.795833, - "weight": 0.0003656939716951409 - }, - { - "days": 0.796528, - "weight": 0.0001296683804018627 - }, - { - "days": 0.797917, - "weight": 0.00015297954991230994 - }, - { - "days": 0.798611, - "weight": 0.0003161577364854405 - }, - { - "days": 0.799306, - "weight": 8.74168856641771e-05 - }, - { - "days": 0.8, - "weight": 0.0001631781865731306 - }, - { - "days": 0.800694, - "weight": 5.536402758731217e-05 - }, - { - "days": 0.801389, - "weight": 0.0003292702693350671 - }, - { - "days": 0.802778, - "weight": 7.721824900335645e-05 - }, - { - "days": 0.804167, - "weight": 9.761552232499777e-05 - }, - { - "days": 0.804861, - "weight": 0.0002986743593526051 - }, - { - "days": 0.805556, - "weight": 7.284740472014759e-05 - }, - { - "days": 0.80625, - "weight": 5.2450131398506267e-05 - }, - { - "days": 0.808333, - "weight": 8.15890932865653e-05 - }, - { - "days": 0.809028, - "weight": 8.304604138096825e-05 - }, - { - "days": 0.810417, - "weight": 0.0003161577364854405 - }, - { - "days": 0.811111, - "weight": 0.00024185338367089 - }, - { - "days": 0.827083, - "weight": 8.887383375858005e-05 - }, - { - "days": 0.832639, - "weight": 0.000151522601817907 - }, - { - "days": 0.836806, - "weight": 6.701961234253578e-05 - }, - { - "days": 0.838194, - "weight": 0.000303045203635814 - }, - { - "days": 0.838889, - "weight": 0.0007809241785999821 - }, - { - "days": 0.839583, - "weight": 0.0004254288435656619 - }, - { - "days": 0.840278, - "weight": 8.15890932865653e-05 - }, - { - "days": 0.840972, - "weight": 4.6622339020894455e-05 - }, - { - "days": 0.841667, - "weight": 0.0001996018889332044 - }, - { - "days": 0.842361, - "weight": 9.324467804178891e-05 - }, - { - "days": 0.84375, - "weight": 0.00030013130744700804 - }, - { - "days": 0.844444, - "weight": 0.00013258227659066862 - }, - { - "days": 0.845833, - "weight": 0.0002301977989156664 - }, - { - "days": 0.846528, - "weight": 0.0006250307324988663 - }, - { - "days": 0.847222, - "weight": 0.0008304604138096825 - }, - { - "days": 0.849306, - "weight": 0.0024957520857122563 - }, - { - "days": 0.85, - "weight": 0.00015297954991230994 - }, - { - "days": 0.850694, - "weight": 0.00043271358403767666 - }, - { - "days": 0.851389, - "weight": 0.0002025157851220103 - }, - { - "days": 0.852778, - "weight": 6.701961234253578e-05 - }, - { - "days": 0.854167, - "weight": 7.721824900335645e-05 - }, - { - "days": 0.854861, - "weight": 0.0004866206635305859 - }, - { - "days": 0.855556, - "weight": 0.00021125747368842801 - }, - { - "days": 0.856944, - "weight": 0.0002564228646149195 - }, - { - "days": 0.857639, - "weight": 9.615857423059482e-05 - }, - { - "days": 0.858333, - "weight": 8.74168856641771e-05 - }, - { - "days": 0.860417, - "weight": 0.00020980052559402507 - }, - { - "days": 0.861111, - "weight": 0.00041523020690484124 - }, - { - "days": 0.861806, - "weight": 0.0001238405880242509 - }, - { - "days": 0.863889, - "weight": 0.00034383975027909664 - }, - { - "days": 0.864583, - "weight": 0.00016754903085633945 - }, - { - "days": 0.865278, - "weight": 0.00027099234555894903 - }, - { - "days": 0.865972, - "weight": 0.00015735039419551879 - }, - { - "days": 0.866667, - "weight": 0.00011364195136343023 - }, - { - "days": 0.868056, - "weight": 0.00014715175753469813 - }, - { - "days": 0.86875, - "weight": 0.0002695353974645461 - }, - { - "days": 0.870139, - "weight": 0.00024185338367089 - }, - { - "days": 0.870833, - "weight": 0.0002170852660660398 - }, - { - "days": 0.872222, - "weight": 0.0002083435774996221 - }, - { - "days": 0.882639, - "weight": 9.761552232499777e-05 - }, - { - "days": 0.890278, - "weight": 5.099318330410331e-05 - }, - { - "days": 0.891667, - "weight": 0.0002899326707861874 - }, - { - "days": 0.896528, - "weight": 0.00042251494737685603 - }, - { - "days": 0.897917, - "weight": 9.615857423059482e-05 - }, - { - "days": 0.898611, - "weight": 0.0003817204007335734 - }, - { - "days": 0.901389, - "weight": 0.00022145611034924868 - }, - { - "days": 0.902083, - "weight": 9.907247041940072e-05 - }, - { - "days": 0.902778, - "weight": 0.00020397273321641325 - }, - { - "days": 0.903472, - "weight": 0.0001937740965555926 - }, - { - "days": 0.904861, - "weight": 0.00030013130744700804 - }, - { - "days": 0.905556, - "weight": 0.0032373386657633587 - }, - { - "days": 0.90625, - "weight": 0.0005099318330410331 - }, - { - "days": 0.906944, - "weight": 0.0005259582620794656 - }, - { - "days": 0.907639, - "weight": 0.0003656939716951409 - }, - { - "days": 0.908333, - "weight": 0.00041231631071603534 - }, - { - "days": 0.909722, - "weight": 6.701961234253578e-05 - }, - { - "days": 0.913194, - "weight": 0.0001660920827619365 - }, - { - "days": 0.913889, - "weight": 0.0001996018889332044 - }, - { - "days": 0.914583, - "weight": 0.0003554953350343202 - }, - { - "days": 0.915278, - "weight": 0.00010052941851380368 - }, - { - "days": 0.915972, - "weight": 0.0007372157357678936 - }, - { - "days": 0.916667, - "weight": 0.00035695228312872316 - }, - { - "days": 0.917361, - "weight": 3.059590998246199e-05 - }, - { - "days": 0.918056, - "weight": 0.00018211851180036898 - }, - { - "days": 0.921528, - "weight": 0.009559036447377767 - }, - { - "days": 0.922222, - "weight": 0.0001908602003667867 - }, - { - "days": 0.922917, - "weight": 0.0007707255419391615 - }, - { - "days": 0.923611, - "weight": 0.0002666215012757402 - }, - { - "days": 0.924306, - "weight": 0.0022233027920589045 - }, - { - "days": 0.925, - "weight": 0.0001748337713283542 - }, - { - "days": 0.925694, - "weight": 0.00041231631071603534 - }, - { - "days": 0.926389, - "weight": 8.887383375858005e-05 - }, - { - "days": 0.927083, - "weight": 0.00017046292704514537 - }, - { - "days": 0.930556, - "weight": 0.0004807928711529741 - }, - { - "days": 0.93125, - "weight": 0.0002841048784085756 - }, - { - "days": 0.931944, - "weight": 0.00015735039419551879 - }, - { - "days": 0.932639, - "weight": 0.0002476811760485018 - }, - { - "days": 0.933333, - "weight": 0.0002841048784085756 - }, - { - "days": 0.94375, - "weight": 0.0003788065045447675 - }, - { - "days": 0.948611, - "weight": 0.0001937740965555926 - }, - { - "days": 0.950694, - "weight": 0.00026225065699253135 - }, - { - "days": 0.951389, - "weight": 0.0010402609394037075 - }, - { - "days": 0.955556, - "weight": 0.000662911382953343 - }, - { - "days": 0.95625, - "weight": 0.002363169809121588 - }, - { - "days": 0.956944, - "weight": 0.0008129770366768471 - }, - { - "days": 0.957639, - "weight": 0.00027827708603096377 - }, - { - "days": 0.958333, - "weight": 0.00018940325227238374 - }, - { - "days": 0.959028, - "weight": 0.0004807928711529741 - }, - { - "days": 0.959722, - "weight": 5.536402758731217e-05 - }, - { - "days": 0.960417, - "weight": 0.0029095253445226947 - }, - { - "days": 0.961111, - "weight": 0.0014554911463085488 - }, - { - "days": 0.961806, - "weight": 0.0001908602003667867 - }, - { - "days": 0.9625, - "weight": 0.001117479188407064 - }, - { - "days": 0.964583, - "weight": 0.0009076786628130389 - }, - { - "days": 0.965278, - "weight": 0.00041960105118805013 - }, - { - "days": 0.965972, - "weight": 0.001269001790224971 - }, - { - "days": 0.968056, - "weight": 0.0007299309952958789 - }, - { - "days": 0.96875, - "weight": 0.005730176855286809 - }, - { - "days": 0.970139, - "weight": 0.0003977468297720058 - }, - { - "days": 0.972222, - "weight": 0.0014438355615533252 - }, - { - "days": 0.972917, - "weight": 0.007031231503588645 - }, - { - "days": 0.973611, - "weight": 0.0012471475688089267 - }, - { - "days": 0.974306, - "weight": 0.001295226855924224 - }, - { - "days": 0.975, - "weight": 0.0006221168363100604 - }, - { - "days": 0.975694, - "weight": 0.00020397273321641325 - }, - { - "days": 0.976389, - "weight": 0.0007663546976559527 - }, - { - "days": 0.977083, - "weight": 0.003211113600064106 - }, - { - "days": 0.977778, - "weight": 3.788065045447675e-05 - }, - { - "days": 0.979167, - "weight": 0.0013753590011163865 - }, - { - "days": 0.979861, - "weight": 0.0007022489815022227 - }, - { - "days": 0.980556, - "weight": 0.0010460887317813193 - }, - { - "days": 0.98125, - "weight": 0.0014001271187212368 - }, - { - "days": 0.981944, - "weight": 0.003171776001515226 - }, - { - "days": 0.982639, - "weight": 0.004177070186653263 - }, - { - "days": 0.983333, - "weight": 0.0009732413270611718 - }, - { - "days": 0.984722, - "weight": 0.00033364111361827595 - }, - { - "days": 0.985417, - "weight": 0.0007648977495615497 - }, - { - "days": 0.986111, - "weight": 0.006868053317015514 - }, - { - "days": 0.986806, - "weight": 0.0002637076050869343 - }, - { - "days": 0.988194, - "weight": 0.00863241745933749 - }, - { - "days": 0.988889, - "weight": 0.000906221714718636 - }, - { - "days": 0.989583, - "weight": 0.0003423828021846937 - }, - { - "days": 0.990278, - "weight": 0.0015021134853294433 - }, - { - "days": 0.990972, - "weight": 0.002775486119837623 - }, - { - "days": 0.991667, - "weight": 3.350980617126789e-05 - }, - { - "days": 0.995139, - "weight": 0.0009207911956626656 - }, - { - "days": 0.995833, - "weight": 0.00011801279564663909 - }, - { - "days": 0.998611, - "weight": 0.00045456780545372093 - }, - { - "days": 1.002778, - "weight": 0.0003423828021846937 - }, - { - "days": 1.007639, - "weight": 0.0012354919840537031 - }, - { - "days": 1.008333, - "weight": 3.788065045447675e-05 - }, - { - "days": 1.009722, - "weight": 0.0027944264450648613 - }, - { - "days": 1.011111, - "weight": 0.00034092585409029074 - }, - { - "days": 1.014583, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.015278, - "weight": 0.001994561941237641 - }, - { - "days": 1.016667, - "weight": 0.00195959518697197 - }, - { - "days": 1.017361, - "weight": 0.0015035704334238462 - }, - { - "days": 1.018056, - "weight": 0.01818416916624324 - }, - { - "days": 1.01875, - "weight": 0.0016827750490354093 - }, - { - "days": 1.020139, - "weight": 0.0005026470925690184 - }, - { - "days": 1.021528, - "weight": 0.00015297954991230994 - }, - { - "days": 1.022917, - "weight": 0.001264630945941762 - }, - { - "days": 1.023611, - "weight": 0.00017629071942275716 - }, - { - "days": 1.024306, - "weight": 0.00033946890599588774 - }, - { - "days": 1.025, - "weight": 0.00022437000653805457 - }, - { - "days": 1.027083, - "weight": 0.000664368331047746 - }, - { - "days": 1.027778, - "weight": 0.004127533951443562 - }, - { - "days": 1.028472, - "weight": 8.74168856641771e-05 - }, - { - "days": 1.029861, - "weight": 0.0017293973880563038 - }, - { - "days": 1.03125, - "weight": 0.00019231714846118964 - }, - { - "days": 1.031944, - "weight": 0.001956681290783164 - }, - { - "days": 1.032639, - "weight": 0.001778933623266004 - }, - { - "days": 1.033333, - "weight": 0.0002141713698772339 - }, - { - "days": 1.034028, - "weight": 0.0010708568493861695 - }, - { - "days": 1.034722, - "weight": 0.004765677216792055 - }, - { - "days": 1.035417, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.036111, - "weight": 0.0011320486693510935 - }, - { - "days": 1.036806, - "weight": 0.00023165474701006934 - }, - { - "days": 1.038889, - "weight": 0.0007372157357678936 - }, - { - "days": 1.039583, - "weight": 0.0009411884689843068 - }, - { - "days": 1.040972, - "weight": 0.00048370676734178 - }, - { - "days": 1.041667, - "weight": 0.0005201304697018537 - }, - { - "days": 1.042361, - "weight": 0.0011757571121831822 - }, - { - "days": 1.043056, - "weight": 0.001873635249402196 - }, - { - "days": 1.04375, - "weight": 0.0011014527593686315 - }, - { - "days": 1.044444, - "weight": 0.0004166871549992442 - }, - { - "days": 1.045139, - "weight": 0.0013564186758891481 - }, - { - "days": 1.045833, - "weight": 0.0005492694315899128 - }, - { - "days": 1.046528, - "weight": 0.0006760239158029696 - }, - { - "days": 1.048611, - "weight": 0.0005303291063626745 - }, - { - "days": 1.05, - "weight": 0.000574037549194763 - }, - { - "days": 1.050694, - "weight": 0.000423971895471259 - }, - { - "days": 1.051389, - "weight": 0.02065661008244505 - }, - { - "days": 1.052083, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.052778, - "weight": 4.516539092649151e-05 - }, - { - "days": 1.054861, - "weight": 0.0026982678708342665 - }, - { - "days": 1.056944, - "weight": 0.00030741604791902283 - }, - { - "days": 1.058333, - "weight": 0.00046622339020894457 - }, - { - "days": 1.064583, - "weight": 0.00023165474701006934 - }, - { - "days": 1.065972, - "weight": 6.410571615372988e-05 - }, - { - "days": 1.069444, - "weight": 0.0004137732588104383 - }, - { - "days": 1.073611, - "weight": 0.0003496675426567084 - }, - { - "days": 1.074306, - "weight": 0.0001150988994578332 - }, - { - "days": 1.076389, - "weight": 0.0003117868922022317 - }, - { - "days": 1.077083, - "weight": 0.0002753631898421579 - }, - { - "days": 1.077778, - "weight": 0.0001748337713283542 - }, - { - "days": 1.079167, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.079861, - "weight": 0.00014860870562910107 - }, - { - "days": 1.08125, - "weight": 9.761552232499777e-05 - }, - { - "days": 1.082639, - "weight": 0.0005492694315899128 - }, - { - "days": 1.084028, - "weight": 6.410571615372988e-05 - }, - { - "days": 1.084722, - "weight": 0.0013972132225324306 - }, - { - "days": 1.086111, - "weight": 0.0002025157851220103 - }, - { - "days": 1.0875, - "weight": 6.847656043693873e-05 - }, - { - "days": 1.089583, - "weight": 0.00032198552886305237 - }, - { - "days": 1.090972, - "weight": 0.00018648935608357782 - }, - { - "days": 1.091667, - "weight": 0.0007736394381279674 - }, - { - "days": 1.092361, - "weight": 0.0011713862678999732 - }, - { - "days": 1.09375, - "weight": 0.0004895345597193918 - }, - { - "days": 1.094444, - "weight": 0.00017046292704514537 - }, - { - "days": 1.095139, - "weight": 0.0002476811760485018 - }, - { - "days": 1.097917, - "weight": 0.0013695312087387747 - }, - { - "days": 1.098611, - "weight": 9.324467804178891e-05 - }, - { - "days": 1.1, - "weight": 0.00015297954991230994 - }, - { - "days": 1.100694, - "weight": 0.00043708442832088556 - }, - { - "days": 1.101389, - "weight": 0.0004181441030936472 - }, - { - "days": 1.102083, - "weight": 0.00035695228312872316 - }, - { - "days": 1.104167, - "weight": 0.00020980052559402507 - }, - { - "days": 1.104861, - "weight": 0.00015006565372350402 - }, - { - "days": 1.105556, - "weight": 7.430435281455054e-05 - }, - { - "days": 1.10625, - "weight": 0.00021271442178283096 - }, - { - "days": 1.108333, - "weight": 0.00021562831797163686 - }, - { - "days": 1.109028, - "weight": 6.993350853134168e-05 - }, - { - "days": 1.109722, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.110417, - "weight": 0.00023165474701006934 - }, - { - "days": 1.111806, - "weight": 0.00018940325227238374 - }, - { - "days": 1.1125, - "weight": 0.00022874085082126344 - }, - { - "days": 1.113889, - "weight": 0.0001384100689682804 - }, - { - "days": 1.115972, - "weight": 7.430435281455054e-05 - }, - { - "days": 1.116667, - "weight": 0.00013258227659066862 - }, - { - "days": 1.118056, - "weight": 0.0004603955978313328 - }, - { - "days": 1.120139, - "weight": 0.00042688579166006487 - }, - { - "days": 1.120833, - "weight": 0.009232680074231505 - }, - { - "days": 1.131944, - "weight": 6.701961234253578e-05 - }, - { - "days": 1.136111, - "weight": 0.0001660920827619365 - }, - { - "days": 1.136806, - "weight": 0.00020397273321641325 - }, - { - "days": 1.1375, - "weight": 0.00016172123847872766 - }, - { - "days": 1.138889, - "weight": 0.0003992037778664088 - }, - { - "days": 1.140278, - "weight": 4.516539092649151e-05 - }, - { - "days": 1.141667, - "weight": 8.74168856641771e-05 - }, - { - "days": 1.145833, - "weight": 0.00011946974374104205 - }, - { - "days": 1.146528, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.147222, - "weight": 9.615857423059482e-05 - }, - { - "days": 1.147917, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.148611, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.149306, - "weight": 9.907247041940072e-05 - }, - { - "days": 1.15, - "weight": 0.0001150988994578332 - }, - { - "days": 1.150694, - "weight": 0.0002928465669749933 - }, - { - "days": 1.151389, - "weight": 0.0005113887811354361 - }, - { - "days": 1.152083, - "weight": 0.00010490026279701253 - }, - { - "days": 1.153472, - "weight": 0.00012821143230745975 - }, - { - "days": 1.154861, - "weight": 0.00011364195136343023 - }, - { - "days": 1.155556, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.156944, - "weight": 4.22514947376856e-05 - }, - { - "days": 1.157639, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.159028, - "weight": 0.0003890051412055881 - }, - { - "days": 1.159722, - "weight": 0.00039046208929999107 - }, - { - "days": 1.160417, - "weight": 0.0012194655550152706 - }, - { - "days": 1.161111, - "weight": 0.00023311169510447228 - }, - { - "days": 1.161806, - "weight": 5.9734871870521025e-05 - }, - { - "days": 1.163194, - "weight": 0.00012529753611865386 - }, - { - "days": 1.163889, - "weight": 0.0002957604631637992 - }, - { - "days": 1.165278, - "weight": 0.0002666215012757402 - }, - { - "days": 1.165972, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.166667, - "weight": 4.079454664328265e-05 - }, - { - "days": 1.167361, - "weight": 4.079454664328265e-05 - }, - { - "days": 1.168056, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.170139, - "weight": 6.847656043693873e-05 - }, - { - "days": 1.170833, - "weight": 0.00010490026279701253 - }, - { - "days": 1.171528, - "weight": 0.00013258227659066862 - }, - { - "days": 1.172917, - "weight": 5.3907079492909214e-05 - }, - { - "days": 1.174306, - "weight": 0.00023456864319887523 - }, - { - "days": 1.176389, - "weight": 0.00018940325227238374 - }, - { - "days": 1.177083, - "weight": 0.00016172123847872766 - }, - { - "days": 1.178472, - "weight": 9.178772994738596e-05 - }, - { - "days": 1.181944, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.184722, - "weight": 4.6622339020894455e-05 - }, - { - "days": 1.1875, - "weight": 0.00015589344610111584 - }, - { - "days": 1.189583, - "weight": 2.476811760485018e-05 - }, - { - "days": 1.191667, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.193056, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.19375, - "weight": 0.00014423786134589223 - }, - { - "days": 1.194444, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.198611, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.199306, - "weight": 0.0015895303709936204 - }, - { - "days": 1.200694, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.202778, - "weight": 8.887383375858005e-05 - }, - { - "days": 1.203472, - "weight": 5.3907079492909214e-05 - }, - { - "days": 1.204861, - "weight": 8.304604138096825e-05 - }, - { - "days": 1.205556, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.206944, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.209028, - "weight": 0.00015589344610111584 - }, - { - "days": 1.209722, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.210417, - "weight": 0.0004137732588104383 - }, - { - "days": 1.211111, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.2125, - "weight": 0.00022582695463245752 - }, - { - "days": 1.213194, - "weight": 9.324467804178891e-05 - }, - { - "days": 1.213889, - "weight": 0.00017046292704514537 - }, - { - "days": 1.214583, - "weight": 0.000120926691835445 - }, - { - "days": 1.215278, - "weight": 0.00010781415898581843 - }, - { - "days": 1.215972, - "weight": 4.22514947376856e-05 - }, - { - "days": 1.218056, - "weight": 0.0001238405880242509 - }, - { - "days": 1.219444, - "weight": 0.0001150988994578332 - }, - { - "days": 1.220139, - "weight": 3.788065045447675e-05 - }, - { - "days": 1.220833, - "weight": 0.0007051628776910286 - }, - { - "days": 1.222222, - "weight": 0.00030158825554141104 - }, - { - "days": 1.222917, - "weight": 5.9734871870521025e-05 - }, - { - "days": 1.224306, - "weight": 0.0001150988994578332 - }, - { - "days": 1.225694, - "weight": 0.0008668841161697563 - }, - { - "days": 1.226389, - "weight": 9.907247041940072e-05 - }, - { - "days": 1.228472, - "weight": 0.000120926691835445 - }, - { - "days": 1.229167, - "weight": 4.516539092649151e-05 - }, - { - "days": 1.229861, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.231944, - "weight": 0.00013549617277947452 - }, - { - "days": 1.232639, - "weight": 8.45029894753712e-05 - }, - { - "days": 1.233333, - "weight": 0.00010927110708022139 - }, - { - "days": 1.234028, - "weight": 0.00013986701706268336 - }, - { - "days": 1.234722, - "weight": 7.139045662574464e-05 - }, - { - "days": 1.2375, - "weight": 0.0003788065045447675 - }, - { - "days": 1.238194, - "weight": 7.430435281455054e-05 - }, - { - "days": 1.238889, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.240278, - "weight": 0.00014715175753469813 - }, - { - "days": 1.246528, - "weight": 0.0015603914091055614 - }, - { - "days": 1.251389, - "weight": 6.264876805932693e-05 - }, - { - "days": 1.254167, - "weight": 0.0001544364980067129 - }, - { - "days": 1.25625, - "weight": 0.00010198636660820662 - }, - { - "days": 1.258333, - "weight": 0.00010635721089141548 - }, - { - "days": 1.261806, - "weight": 0.00022874085082126344 - }, - { - "days": 1.263889, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.264583, - "weight": 2.9138961888059035e-05 - }, - { - "days": 1.265972, - "weight": 9.761552232499777e-05 - }, - { - "days": 1.267361, - "weight": 0.0002170852660660398 - }, - { - "days": 1.268056, - "weight": 0.00017191987513954832 - }, - { - "days": 1.26875, - "weight": 0.00011218500326902729 - }, - { - "days": 1.270139, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.272222, - "weight": 9.178772994738596e-05 - }, - { - "days": 1.273611, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.274306, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.275, - "weight": 0.0001238405880242509 - }, - { - "days": 1.276389, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.277083, - "weight": 0.00013695312087387746 - }, - { - "days": 1.278472, - "weight": 8.45029894753712e-05 - }, - { - "days": 1.279167, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.279861, - "weight": 4.079454664328265e-05 - }, - { - "days": 1.281944, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.282639, - "weight": 0.00028119098221976967 - }, - { - "days": 1.283333, - "weight": 9.761552232499777e-05 - }, - { - "days": 1.284028, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.284722, - "weight": 0.00033946890599588774 - }, - { - "days": 1.285417, - "weight": 0.0001267544842130568 - }, - { - "days": 1.286111, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.288194, - "weight": 0.0002928465669749933 - }, - { - "days": 1.290278, - "weight": 4.079454664328265e-05 - }, - { - "days": 1.29375, - "weight": 0.00012238363992984796 - }, - { - "days": 1.294444, - "weight": 0.00010490026279701253 - }, - { - "days": 1.295139, - "weight": 3.788065045447675e-05 - }, - { - "days": 1.295833, - "weight": 4.6622339020894455e-05 - }, - { - "days": 1.296528, - "weight": 0.000151522601817907 - }, - { - "days": 1.297917, - "weight": 0.00010490026279701253 - }, - { - "days": 1.298611, - "weight": 0.0004705942344921534 - }, - { - "days": 1.299306, - "weight": 0.00013549617277947452 - }, - { - "days": 1.303472, - "weight": 0.00022728390272686047 - }, - { - "days": 1.306944, - "weight": 9.470162613619187e-05 - }, - { - "days": 1.313194, - "weight": 8.595993756977416e-05 - }, - { - "days": 1.315972, - "weight": 5.9734871870521025e-05 - }, - { - "days": 1.316667, - "weight": 4.516539092649151e-05 - }, - { - "days": 1.317361, - "weight": 4.22514947376856e-05 - }, - { - "days": 1.322917, - "weight": 8.595993756977416e-05 - }, - { - "days": 1.325, - "weight": 6.410571615372988e-05 - }, - { - "days": 1.325694, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.326389, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.327083, - "weight": 8.304604138096825e-05 - }, - { - "days": 1.327778, - "weight": 2.7682013793656084e-05 - }, - { - "days": 1.328472, - "weight": 0.00014715175753469813 - }, - { - "days": 1.329167, - "weight": 4.516539092649151e-05 - }, - { - "days": 1.329861, - "weight": 8.304604138096825e-05 - }, - { - "days": 1.330556, - "weight": 9.033078185298302e-05 - }, - { - "days": 1.33125, - "weight": 0.0005813222896667778 - }, - { - "days": 1.332639, - "weight": 9.907247041940072e-05 - }, - { - "days": 1.333333, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.334722, - "weight": 0.00010927110708022139 - }, - { - "days": 1.336806, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.338194, - "weight": 9.907247041940072e-05 - }, - { - "days": 1.338889, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.339583, - "weight": 7.57613009089535e-05 - }, - { - "days": 1.340278, - "weight": 4.516539092649151e-05 - }, - { - "days": 1.340972, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.342361, - "weight": 0.0003234424769574553 - }, - { - "days": 1.343056, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.344444, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.345139, - "weight": 0.00010490026279701253 - }, - { - "days": 1.346528, - "weight": 0.0001748337713283542 - }, - { - "days": 1.348611, - "weight": 0.0001908602003667867 - }, - { - "days": 1.35, - "weight": 0.00016754903085633945 - }, - { - "days": 1.352083, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.352778, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.353472, - "weight": 8.595993756977416e-05 - }, - { - "days": 1.354167, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.354861, - "weight": 5.536402758731217e-05 - }, - { - "days": 1.355556, - "weight": 8.887383375858005e-05 - }, - { - "days": 1.357639, - "weight": 0.00013258227659066862 - }, - { - "days": 1.360417, - "weight": 0.0004647664421145416 - }, - { - "days": 1.361111, - "weight": 9.470162613619187e-05 - }, - { - "days": 1.363194, - "weight": 0.00010052941851380368 - }, - { - "days": 1.372222, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.372917, - "weight": 0.00010344331470260957 - }, - { - "days": 1.377083, - "weight": 4.6622339020894455e-05 - }, - { - "days": 1.379861, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.382639, - "weight": 8.304604138096825e-05 - }, - { - "days": 1.385417, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.386111, - "weight": 3.496675426567084e-05 - }, - { - "days": 1.386806, - "weight": 4.079454664328265e-05 - }, - { - "days": 1.388194, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.388889, - "weight": 3.788065045447675e-05 - }, - { - "days": 1.389583, - "weight": 7.430435281455054e-05 - }, - { - "days": 1.390972, - "weight": 5.9734871870521025e-05 - }, - { - "days": 1.391667, - "weight": 9.178772994738596e-05 - }, - { - "days": 1.392361, - "weight": 0.00011218500326902729 - }, - { - "days": 1.393056, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.395139, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.396528, - "weight": 2.7682013793656084e-05 - }, - { - "days": 1.397222, - "weight": 0.0002564228646149195 - }, - { - "days": 1.397917, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.398611, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.399306, - "weight": 0.0003686078678839468 - }, - { - "days": 1.4, - "weight": 7.139045662574464e-05 - }, - { - "days": 1.402083, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.402778, - "weight": 2.476811760485018e-05 - }, - { - "days": 1.403472, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.404167, - "weight": 0.0002695353974645461 - }, - { - "days": 1.405556, - "weight": 3.496675426567084e-05 - }, - { - "days": 1.407639, - "weight": 9.761552232499777e-05 - }, - { - "days": 1.409028, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.409722, - "weight": 2.7682013793656084e-05 - }, - { - "days": 1.410417, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.4125, - "weight": 0.00013258227659066862 - }, - { - "days": 1.413194, - "weight": 0.00010052941851380368 - }, - { - "days": 1.414583, - "weight": 0.00019814494083880143 - }, - { - "days": 1.415972, - "weight": 2.9138961888059035e-05 - }, - { - "days": 1.418056, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.41875, - "weight": 4.807928711529741e-05 - }, - { - "days": 1.419444, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.420139, - "weight": 0.0003059590998246199 - }, - { - "days": 1.426389, - "weight": 9.615857423059482e-05 - }, - { - "days": 1.430556, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.435417, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.4375, - "weight": 3.496675426567084e-05 - }, - { - "days": 1.440278, - "weight": 3.788065045447675e-05 - }, - { - "days": 1.441667, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.445833, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.447222, - "weight": 3.788065045447675e-05 - }, - { - "days": 1.448611, - "weight": 0.0002535089684261136 - }, - { - "days": 1.449306, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.45, - "weight": 4.516539092649151e-05 - }, - { - "days": 1.451389, - "weight": 0.0001267544842130568 - }, - { - "days": 1.453472, - "weight": 6.410571615372988e-05 - }, - { - "days": 1.454167, - "weight": 0.00017629071942275716 - }, - { - "days": 1.45625, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.456944, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.458333, - "weight": 0.00011655584755223614 - }, - { - "days": 1.460417, - "weight": 0.00026225065699253135 - }, - { - "days": 1.461111, - "weight": 0.00023456864319887523 - }, - { - "days": 1.463889, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.464583, - "weight": 6.993350853134168e-05 - }, - { - "days": 1.465972, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.468056, - "weight": 5.536402758731217e-05 - }, - { - "days": 1.469444, - "weight": 5.536402758731217e-05 - }, - { - "days": 1.470139, - "weight": 0.00018940325227238374 - }, - { - "days": 1.471528, - "weight": 0.000120926691835445 - }, - { - "days": 1.472222, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.472917, - "weight": 4.079454664328265e-05 - }, - { - "days": 1.474306, - "weight": 4.807928711529741e-05 - }, - { - "days": 1.475694, - "weight": 6.701961234253578e-05 - }, - { - "days": 1.476389, - "weight": 3.496675426567084e-05 - }, - { - "days": 1.477778, - "weight": 8.304604138096825e-05 - }, - { - "days": 1.479167, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.48125, - "weight": 0.00034383975027909664 - }, - { - "days": 1.482639, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.495833, - "weight": 5.9734871870521025e-05 - }, - { - "days": 1.498611, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.499306, - "weight": 0.00017046292704514537 - }, - { - "days": 1.504167, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.507639, - "weight": 7.57613009089535e-05 - }, - { - "days": 1.508333, - "weight": 7.430435281455054e-05 - }, - { - "days": 1.509028, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.509722, - "weight": 8.595993756977416e-05 - }, - { - "days": 1.510417, - "weight": 2.7682013793656084e-05 - }, - { - "days": 1.513194, - "weight": 9.615857423059482e-05 - }, - { - "days": 1.513889, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.515278, - "weight": 2.9138961888059035e-05 - }, - { - "days": 1.518056, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.51875, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.519444, - "weight": 9.761552232499777e-05 - }, - { - "days": 1.520139, - "weight": 0.0003948329335831999 - }, - { - "days": 1.521528, - "weight": 9.033078185298302e-05 - }, - { - "days": 1.522222, - "weight": 0.00013986701706268336 - }, - { - "days": 1.523611, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.524306, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.527083, - "weight": 0.00010927110708022139 - }, - { - "days": 1.527778, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.528472, - "weight": 6.264876805932693e-05 - }, - { - "days": 1.529167, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.530556, - "weight": 3.788065045447675e-05 - }, - { - "days": 1.53125, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.532639, - "weight": 0.00010781415898581843 - }, - { - "days": 1.534028, - "weight": 5.9734871870521025e-05 - }, - { - "days": 1.534722, - "weight": 6.993350853134168e-05 - }, - { - "days": 1.535417, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.536111, - "weight": 6.264876805932693e-05 - }, - { - "days": 1.536806, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.538194, - "weight": 0.00036715091978954385 - }, - { - "days": 1.545833, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.550694, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.55625, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.558333, - "weight": 0.0003452966983734996 - }, - { - "days": 1.564583, - "weight": 3.059590998246199e-05 - }, - { - "days": 1.565972, - "weight": 4.079454664328265e-05 - }, - { - "days": 1.567361, - "weight": 4.516539092649151e-05 - }, - { - "days": 1.568056, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.570139, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.572917, - "weight": 0.00011946974374104205 - }, - { - "days": 1.574306, - "weight": 6.410571615372988e-05 - }, - { - "days": 1.577778, - "weight": 3.788065045447675e-05 - }, - { - "days": 1.578472, - "weight": 4.22514947376856e-05 - }, - { - "days": 1.579167, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.580556, - "weight": 0.0001238405880242509 - }, - { - "days": 1.581944, - "weight": 0.0004050315702440206 - }, - { - "days": 1.582639, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.583333, - "weight": 6.410571615372988e-05 - }, - { - "days": 1.585417, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.586806, - "weight": 2.9138961888059035e-05 - }, - { - "days": 1.5875, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.588194, - "weight": 0.00012821143230745975 - }, - { - "days": 1.590972, - "weight": 8.45029894753712e-05 - }, - { - "days": 1.592361, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.593056, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.594444, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.595833, - "weight": 0.00033072721742947005 - }, - { - "days": 1.596528, - "weight": 6.264876805932693e-05 - }, - { - "days": 1.597917, - "weight": 6.410571615372988e-05 - }, - { - "days": 1.599306, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.6, - "weight": 2.7682013793656084e-05 - }, - { - "days": 1.601389, - "weight": 2.476811760485018e-05 - }, - { - "days": 1.615278, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.621528, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.625, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.627083, - "weight": 3.059590998246199e-05 - }, - { - "days": 1.634722, - "weight": 8.45029894753712e-05 - }, - { - "days": 1.6375, - "weight": 2.9138961888059035e-05 - }, - { - "days": 1.639583, - "weight": 5.9734871870521025e-05 - }, - { - "days": 1.640972, - "weight": 6.264876805932693e-05 - }, - { - "days": 1.641667, - "weight": 2.7682013793656084e-05 - }, - { - "days": 1.642361, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.64375, - "weight": 0.00013695312087387746 - }, - { - "days": 1.644444, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.645833, - "weight": 5.536402758731217e-05 - }, - { - "days": 1.646528, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.647917, - "weight": 0.00017920461561156306 - }, - { - "days": 1.650694, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.653472, - "weight": 8.15890932865653e-05 - }, - { - "days": 1.654861, - "weight": 0.00017920461561156306 - }, - { - "days": 1.655556, - "weight": 0.00013403922468507157 - }, - { - "days": 1.65625, - "weight": 4.807928711529741e-05 - }, - { - "days": 1.656944, - "weight": 9.761552232499777e-05 - }, - { - "days": 1.658333, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.659722, - "weight": 0.0003846342969223793 - }, - { - "days": 1.6625, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.668056, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.670833, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.671528, - "weight": 0.00011218500326902729 - }, - { - "days": 1.676389, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.684028, - "weight": 2.7682013793656084e-05 - }, - { - "days": 1.689583, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.691667, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.69375, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.696528, - "weight": 3.496675426567084e-05 - }, - { - "days": 1.697917, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.698611, - "weight": 3.059590998246199e-05 - }, - { - "days": 1.699306, - "weight": 7.57613009089535e-05 - }, - { - "days": 1.700694, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.703472, - "weight": 8.595993756977416e-05 - }, - { - "days": 1.704861, - "weight": 3.496675426567084e-05 - }, - { - "days": 1.705556, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.709722, - "weight": 0.00015880734228992173 - }, - { - "days": 1.711806, - "weight": 9.761552232499777e-05 - }, - { - "days": 1.7125, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.713889, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.714583, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.715278, - "weight": 2.7682013793656084e-05 - }, - { - "days": 1.715972, - "weight": 8.304604138096825e-05 - }, - { - "days": 1.716667, - "weight": 0.00015297954991230994 - }, - { - "days": 1.71875, - "weight": 3.496675426567084e-05 - }, - { - "days": 1.720139, - "weight": 0.000633772421065284 - }, - { - "days": 1.721528, - "weight": 5.2450131398506267e-05 - }, - { - "days": 1.731944, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.735417, - "weight": 9.324467804178891e-05 - }, - { - "days": 1.738889, - "weight": 5.9734871870521025e-05 - }, - { - "days": 1.743056, - "weight": 3.059590998246199e-05 - }, - { - "days": 1.744444, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.748611, - "weight": 4.079454664328265e-05 - }, - { - "days": 1.757639, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.758333, - "weight": 5.827792377611807e-05 - }, - { - "days": 1.760417, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.761111, - "weight": 6.993350853134168e-05 - }, - { - "days": 1.7625, - "weight": 0.00017337682323395127 - }, - { - "days": 1.763194, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.766667, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.772222, - "weight": 6.993350853134168e-05 - }, - { - "days": 1.773611, - "weight": 0.00011655584755223614 - }, - { - "days": 1.775, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.775694, - "weight": 8.15890932865653e-05 - }, - { - "days": 1.777083, - "weight": 7.721824900335645e-05 - }, - { - "days": 1.777778, - "weight": 8.013214519216235e-05 - }, - { - "days": 1.779167, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.781944, - "weight": 0.0001602642903843247 - }, - { - "days": 1.782639, - "weight": 0.000543441639212301 - }, - { - "days": 1.789583, - "weight": 0.00010198636660820662 - }, - { - "days": 1.810417, - "weight": 4.807928711529741e-05 - }, - { - "days": 1.813194, - "weight": 3.059590998246199e-05 - }, - { - "days": 1.815278, - "weight": 3.205285807686494e-05 - }, - { - "days": 1.819444, - "weight": 8.887383375858005e-05 - }, - { - "days": 1.820833, - "weight": 3.059590998246199e-05 - }, - { - "days": 1.821528, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.823611, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.825, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.825694, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.827083, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.828472, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.829167, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.83125, - "weight": 4.6622339020894455e-05 - }, - { - "days": 1.831944, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.834028, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.834722, - "weight": 9.033078185298302e-05 - }, - { - "days": 1.835417, - "weight": 3.496675426567084e-05 - }, - { - "days": 1.836806, - "weight": 0.0001631781865731306 - }, - { - "days": 1.8375, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.838889, - "weight": 6.993350853134168e-05 - }, - { - "days": 1.839583, - "weight": 4.22514947376856e-05 - }, - { - "days": 1.840278, - "weight": 7.430435281455054e-05 - }, - { - "days": 1.840972, - "weight": 0.0001267544842130568 - }, - { - "days": 1.844444, - "weight": 0.0005638389125339424 - }, - { - "days": 1.845833, - "weight": 3.788065045447675e-05 - }, - { - "days": 1.847917, - "weight": 6.701961234253578e-05 - }, - { - "days": 1.849306, - "weight": 4.6622339020894455e-05 - }, - { - "days": 1.85625, - "weight": 7.867519709775939e-05 - }, - { - "days": 1.857639, - "weight": 3.059590998246199e-05 - }, - { - "days": 1.863889, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.867361, - "weight": 3.059590998246199e-05 - }, - { - "days": 1.868056, - "weight": 4.6622339020894455e-05 - }, - { - "days": 1.872222, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.872917, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.878472, - "weight": 9.324467804178891e-05 - }, - { - "days": 1.880556, - "weight": 3.9337598548879697e-05 - }, - { - "days": 1.88125, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.882639, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.883333, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.884722, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.885417, - "weight": 2.9138961888059035e-05 - }, - { - "days": 1.886806, - "weight": 2.9138961888059035e-05 - }, - { - "days": 1.888194, - "weight": 5.3907079492909214e-05 - }, - { - "days": 1.890278, - "weight": 0.00022437000653805457 - }, - { - "days": 1.891667, - "weight": 9.033078185298302e-05 - }, - { - "days": 1.894444, - "weight": 4.6622339020894455e-05 - }, - { - "days": 1.895833, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.897222, - "weight": 6.264876805932693e-05 - }, - { - "days": 1.897917, - "weight": 0.00022437000653805457 - }, - { - "days": 1.898611, - "weight": 7.284740472014759e-05 - }, - { - "days": 1.899306, - "weight": 0.0001544364980067129 - }, - { - "days": 1.9, - "weight": 4.079454664328265e-05 - }, - { - "days": 1.900694, - "weight": 0.00011072805517462434 - }, - { - "days": 1.906944, - "weight": 8.887383375858005e-05 - }, - { - "days": 1.907639, - "weight": 0.0008741688566417711 - }, - { - "days": 1.908333, - "weight": 4.516539092649151e-05 - }, - { - "days": 1.909722, - "weight": 4.6622339020894455e-05 - }, - { - "days": 1.916667, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.921528, - "weight": 2.476811760485018e-05 - }, - { - "days": 1.922917, - "weight": 6.264876805932693e-05 - }, - { - "days": 1.923611, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.925694, - "weight": 2.7682013793656084e-05 - }, - { - "days": 1.932639, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.933333, - "weight": 0.00010781415898581843 - }, - { - "days": 1.936111, - "weight": 3.6423702360073794e-05 - }, - { - "days": 1.939583, - "weight": 4.6622339020894455e-05 - }, - { - "days": 1.940278, - "weight": 0.00010198636660820662 - }, - { - "days": 1.941667, - "weight": 3.059590998246199e-05 - }, - { - "days": 1.942361, - "weight": 8.595993756977416e-05 - }, - { - "days": 1.943056, - "weight": 4.807928711529741e-05 - }, - { - "days": 1.945139, - "weight": 4.807928711529741e-05 - }, - { - "days": 1.945833, - "weight": 9.033078185298302e-05 - }, - { - "days": 1.946528, - "weight": 0.00014278091325148928 - }, - { - "days": 1.947222, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.947917, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.949306, - "weight": 6.119181996492398e-05 - }, - { - "days": 1.95, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.950694, - "weight": 8.15890932865653e-05 - }, - { - "days": 1.951389, - "weight": 8.887383375858005e-05 - }, - { - "days": 1.954861, - "weight": 0.00018794630417798077 - }, - { - "days": 1.955556, - "weight": 3.350980617126789e-05 - }, - { - "days": 1.956944, - "weight": 0.00012821143230745975 - }, - { - "days": 1.958333, - "weight": 5.536402758731217e-05 - }, - { - "days": 1.959028, - "weight": 0.00037006481597834975 - }, - { - "days": 1.959722, - "weight": 0.00015006565372350402 - }, - { - "days": 1.961111, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.963194, - "weight": 2.6225065699253133e-05 - }, - { - "days": 1.963889, - "weight": 4.079454664328265e-05 - }, - { - "days": 1.964583, - "weight": 0.00018503240798917487 - }, - { - "days": 1.965972, - "weight": 0.0003656939716951409 - }, - { - "days": 1.966667, - "weight": 4.370844283208855e-05 - }, - { - "days": 1.968056, - "weight": 4.22514947376856e-05 - }, - { - "days": 1.970139, - "weight": 4.953623520970036e-05 - }, - { - "days": 1.970833, - "weight": 4.22514947376856e-05 - }, - { - "days": 1.972917, - "weight": 8.45029894753712e-05 - }, - { - "days": 1.975694, - "weight": 3.496675426567084e-05 - }, - { - "days": 1.98125, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.981944, - "weight": 6.556266424813284e-05 - }, - { - "days": 1.982639, - "weight": 0.00016172123847872766 - }, - { - "days": 1.9875, - "weight": 8.45029894753712e-05 - }, - { - "days": 1.988194, - "weight": 8.887383375858005e-05 - }, - { - "days": 1.990972, - "weight": 5.3907079492909214e-05 - }, - { - "days": 1.99375, - "weight": 9.615857423059482e-05 - }, - { - "days": 1.997222, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.997917, - "weight": 5.6820975681715117e-05 - }, - { - "days": 1.998611, - "weight": 0.00022437000653805457 - }, - { - "days": 1.999306, - "weight": 7.57613009089535e-05 - }, - { - "days": 2.001389, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.002083, - "weight": 0.00010344331470260957 - }, - { - "days": 2.004167, - "weight": 0.00034092585409029074 - }, - { - "days": 2.004861, - "weight": 0.0001748337713283542 - }, - { - "days": 2.005556, - "weight": 3.059590998246199e-05 - }, - { - "days": 2.006944, - "weight": 3.205285807686494e-05 - }, - { - "days": 2.008333, - "weight": 7.284740472014759e-05 - }, - { - "days": 2.009028, - "weight": 0.00017191987513954832 - }, - { - "days": 2.009722, - "weight": 0.00014860870562910107 - }, - { - "days": 2.010417, - "weight": 0.00012238363992984796 - }, - { - "days": 2.013194, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.014583, - "weight": 6.119181996492398e-05 - }, - { - "days": 2.015278, - "weight": 4.807928711529741e-05 - }, - { - "days": 2.015972, - "weight": 0.00023311169510447228 - }, - { - "days": 2.018056, - "weight": 5.2450131398506267e-05 - }, - { - "days": 2.01875, - "weight": 8.74168856641771e-05 - }, - { - "days": 2.020139, - "weight": 0.00010781415898581843 - }, - { - "days": 2.020833, - "weight": 0.00030450215173021694 - }, - { - "days": 2.023611, - "weight": 0.00010781415898581843 - }, - { - "days": 2.024306, - "weight": 5.3907079492909214e-05 - }, - { - "days": 2.025694, - "weight": 0.0004560247535481239 - }, - { - "days": 2.03125, - "weight": 0.000272449293653352 - }, - { - "days": 2.031944, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.034028, - "weight": 8.304604138096825e-05 - }, - { - "days": 2.041667, - "weight": 5.6820975681715117e-05 - }, - { - "days": 2.042361, - "weight": 8.304604138096825e-05 - }, - { - "days": 2.044444, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.045139, - "weight": 4.516539092649151e-05 - }, - { - "days": 2.046528, - "weight": 4.079454664328265e-05 - }, - { - "days": 2.048611, - "weight": 4.22514947376856e-05 - }, - { - "days": 2.049306, - "weight": 6.556266424813284e-05 - }, - { - "days": 2.052778, - "weight": 7.721824900335645e-05 - }, - { - "days": 2.053472, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.054167, - "weight": 3.205285807686494e-05 - }, - { - "days": 2.056944, - "weight": 8.887383375858005e-05 - }, - { - "days": 2.057639, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.059028, - "weight": 3.205285807686494e-05 - }, - { - "days": 2.059722, - "weight": 6.556266424813284e-05 - }, - { - "days": 2.063889, - "weight": 0.00010344331470260957 - }, - { - "days": 2.065278, - "weight": 5.3907079492909214e-05 - }, - { - "days": 2.065972, - "weight": 3.205285807686494e-05 - }, - { - "days": 2.066667, - "weight": 6.264876805932693e-05 - }, - { - "days": 2.068056, - "weight": 9.615857423059482e-05 - }, - { - "days": 2.070833, - "weight": 0.00014715175753469813 - }, - { - "days": 2.071528, - "weight": 5.6820975681715117e-05 - }, - { - "days": 2.072917, - "weight": 6.993350853134168e-05 - }, - { - "days": 2.073611, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.075, - "weight": 0.00014715175753469813 - }, - { - "days": 2.077083, - "weight": 0.00010490026279701253 - }, - { - "days": 2.078472, - "weight": 8.887383375858005e-05 - }, - { - "days": 2.079861, - "weight": 7.57613009089535e-05 - }, - { - "days": 2.080556, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.082639, - "weight": 6.264876805932693e-05 - }, - { - "days": 2.083333, - "weight": 2.476811760485018e-05 - }, - { - "days": 2.086806, - "weight": 0.0003205285807686494 - }, - { - "days": 2.093056, - "weight": 0.00045165390926491504 - }, - { - "days": 2.095139, - "weight": 3.059590998246199e-05 - }, - { - "days": 2.108333, - "weight": 6.993350853134168e-05 - }, - { - "days": 2.110417, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.115972, - "weight": 4.079454664328265e-05 - }, - { - "days": 2.117361, - "weight": 2.476811760485018e-05 - }, - { - "days": 2.11875, - "weight": 0.0001544364980067129 - }, - { - "days": 2.120833, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.122917, - "weight": 2.6225065699253133e-05 - }, - { - "days": 2.125, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.126389, - "weight": 3.059590998246199e-05 - }, - { - "days": 2.127778, - "weight": 5.827792377611807e-05 - }, - { - "days": 2.129861, - "weight": 4.079454664328265e-05 - }, - { - "days": 2.130556, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.132639, - "weight": 2.6225065699253133e-05 - }, - { - "days": 2.134028, - "weight": 5.2450131398506267e-05 - }, - { - "days": 2.136111, - "weight": 9.324467804178891e-05 - }, - { - "days": 2.1375, - "weight": 0.00013403922468507157 - }, - { - "days": 2.138194, - "weight": 7.57613009089535e-05 - }, - { - "days": 2.138889, - "weight": 3.788065045447675e-05 - }, - { - "days": 2.140278, - "weight": 0.0001384100689682804 - }, - { - "days": 2.140972, - "weight": 4.22514947376856e-05 - }, - { - "days": 2.145139, - "weight": 3.788065045447675e-05 - }, - { - "days": 2.15, - "weight": 0.00010927110708022139 - }, - { - "days": 2.152778, - "weight": 2.6225065699253133e-05 - }, - { - "days": 2.154861, - "weight": 0.0001660920827619365 - }, - { - "days": 2.159028, - "weight": 9.033078185298302e-05 - }, - { - "days": 2.164583, - "weight": 3.205285807686494e-05 - }, - { - "days": 2.165972, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.168056, - "weight": 2.7682013793656084e-05 - }, - { - "days": 2.178472, - "weight": 2.476811760485018e-05 - }, - { - "days": 2.180556, - "weight": 5.2450131398506267e-05 - }, - { - "days": 2.181944, - "weight": 9.615857423059482e-05 - }, - { - "days": 2.182639, - "weight": 4.807928711529741e-05 - }, - { - "days": 2.184722, - "weight": 9.761552232499777e-05 - }, - { - "days": 2.188194, - "weight": 3.059590998246199e-05 - }, - { - "days": 2.189583, - "weight": 5.3907079492909214e-05 - }, - { - "days": 2.190972, - "weight": 5.827792377611807e-05 - }, - { - "days": 2.191667, - "weight": 3.205285807686494e-05 - }, - { - "days": 2.195139, - "weight": 5.536402758731217e-05 - }, - { - "days": 2.196528, - "weight": 2.6225065699253133e-05 - }, - { - "days": 2.197222, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.197917, - "weight": 3.205285807686494e-05 - }, - { - "days": 2.198611, - "weight": 5.9734871870521025e-05 - }, - { - "days": 2.199306, - "weight": 4.953623520970036e-05 - }, - { - "days": 2.2, - "weight": 5.6820975681715117e-05 - }, - { - "days": 2.204167, - "weight": 3.496675426567084e-05 - }, - { - "days": 2.206944, - "weight": 0.0001238405880242509 - }, - { - "days": 2.210417, - "weight": 0.00023165474701006934 - }, - { - "days": 2.211806, - "weight": 2.476811760485018e-05 - }, - { - "days": 2.220833, - "weight": 0.00011801279564663909 - }, - { - "days": 2.238889, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.241667, - "weight": 4.22514947376856e-05 - }, - { - "days": 2.24375, - "weight": 5.9734871870521025e-05 - }, - { - "days": 2.245139, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.246528, - "weight": 4.516539092649151e-05 - }, - { - "days": 2.247917, - "weight": 8.15890932865653e-05 - }, - { - "days": 2.250694, - "weight": 5.2450131398506267e-05 - }, - { - "days": 2.252778, - "weight": 4.6622339020894455e-05 - }, - { - "days": 2.253472, - "weight": 3.496675426567084e-05 - }, - { - "days": 2.254167, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.25625, - "weight": 6.993350853134168e-05 - }, - { - "days": 2.256944, - "weight": 0.00011364195136343023 - }, - { - "days": 2.258333, - "weight": 3.496675426567084e-05 - }, - { - "days": 2.259028, - "weight": 4.516539092649151e-05 - }, - { - "days": 2.261806, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.266667, - "weight": 8.595993756977416e-05 - }, - { - "days": 2.269444, - "weight": 0.00019231714846118964 - }, - { - "days": 2.272222, - "weight": 0.0001602642903843247 - }, - { - "days": 2.299306, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.302778, - "weight": 4.953623520970036e-05 - }, - { - "days": 2.309722, - "weight": 3.788065045447675e-05 - }, - { - "days": 2.311806, - "weight": 6.119181996492398e-05 - }, - { - "days": 2.3125, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.313194, - "weight": 2.7682013793656084e-05 - }, - { - "days": 2.314583, - "weight": 5.3907079492909214e-05 - }, - { - "days": 2.315972, - "weight": 5.3907079492909214e-05 - }, - { - "days": 2.318056, - "weight": 0.00010927110708022139 - }, - { - "days": 2.31875, - "weight": 4.6622339020894455e-05 - }, - { - "days": 2.325694, - "weight": 8.304604138096825e-05 - }, - { - "days": 2.330556, - "weight": 0.0002170852660660398 - }, - { - "days": 2.33125, - "weight": 0.00010781415898581843 - }, - { - "days": 2.336111, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.338194, - "weight": 0.00010490026279701253 - }, - { - "days": 2.360417, - "weight": 4.6622339020894455e-05 - }, - { - "days": 2.36875, - "weight": 4.22514947376856e-05 - }, - { - "days": 2.373611, - "weight": 9.033078185298302e-05 - }, - { - "days": 2.375, - "weight": 2.7682013793656084e-05 - }, - { - "days": 2.376389, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.379167, - "weight": 6.701961234253578e-05 - }, - { - "days": 2.379861, - "weight": 2.7682013793656084e-05 - }, - { - "days": 2.384722, - "weight": 0.00014715175753469813 - }, - { - "days": 2.385417, - "weight": 4.22514947376856e-05 - }, - { - "days": 2.386111, - "weight": 0.0002301977989156664 - }, - { - "days": 2.392361, - "weight": 0.0001238405880242509 - }, - { - "days": 2.39375, - "weight": 2.6225065699253133e-05 - }, - { - "days": 2.397222, - "weight": 6.847656043693873e-05 - }, - { - "days": 2.421528, - "weight": 4.079454664328265e-05 - }, - { - "days": 2.43125, - "weight": 6.847656043693873e-05 - }, - { - "days": 2.432639, - "weight": 5.3907079492909214e-05 - }, - { - "days": 2.434722, - "weight": 6.993350853134168e-05 - }, - { - "days": 2.435417, - "weight": 2.476811760485018e-05 - }, - { - "days": 2.436806, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.4375, - "weight": 3.496675426567084e-05 - }, - { - "days": 2.440278, - "weight": 0.00015297954991230994 - }, - { - "days": 2.44375, - "weight": 3.496675426567084e-05 - }, - { - "days": 2.447917, - "weight": 0.00010635721089141548 - }, - { - "days": 2.448611, - "weight": 3.496675426567084e-05 - }, - { - "days": 2.450694, - "weight": 0.00023748253938768113 - }, - { - "days": 2.451389, - "weight": 4.953623520970036e-05 - }, - { - "days": 2.458333, - "weight": 0.000120926691835445 - }, - { - "days": 2.478472, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.49375, - "weight": 3.496675426567084e-05 - }, - { - "days": 2.495139, - "weight": 2.6225065699253133e-05 - }, - { - "days": 2.495833, - "weight": 6.410571615372988e-05 - }, - { - "days": 2.496528, - "weight": 5.536402758731217e-05 - }, - { - "days": 2.499306, - "weight": 0.00023748253938768113 - }, - { - "days": 2.501389, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.505556, - "weight": 0.0003190716326742464 - }, - { - "days": 2.509722, - "weight": 5.6820975681715117e-05 - }, - { - "days": 2.511111, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.5125, - "weight": 0.00012238363992984796 - }, - { - "days": 2.513194, - "weight": 3.9337598548879697e-05 - }, - { - "days": 2.513889, - "weight": 9.324467804178891e-05 - }, - { - "days": 2.532639, - "weight": 3.205285807686494e-05 - }, - { - "days": 2.538194, - "weight": 3.496675426567084e-05 - }, - { - "days": 2.552083, - "weight": 2.6225065699253133e-05 - }, - { - "days": 2.554167, - "weight": 4.516539092649151e-05 - }, - { - "days": 2.557639, - "weight": 3.496675426567084e-05 - }, - { - "days": 2.561111, - "weight": 2.7682013793656084e-05 - }, - { - "days": 2.5625, - "weight": 5.2450131398506267e-05 - }, - { - "days": 2.565278, - "weight": 6.264876805932693e-05 - }, - { - "days": 2.565972, - "weight": 0.0004079454664328265 - }, - { - "days": 2.567361, - "weight": 0.00018357545989477193 - }, - { - "days": 2.568056, - "weight": 4.807928711529741e-05 - }, - { - "days": 2.570139, - "weight": 7.139045662574464e-05 - }, - { - "days": 2.570833, - "weight": 0.00017337682323395127 - }, - { - "days": 2.578472, - "weight": 0.0001296683804018627 - }, - { - "days": 2.602083, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.620139, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.621528, - "weight": 0.00015880734228992173 - }, - { - "days": 2.628472, - "weight": 0.00016172123847872766 - }, - { - "days": 2.629167, - "weight": 8.15890932865653e-05 - }, - { - "days": 2.629861, - "weight": 0.00010635721089141548 - }, - { - "days": 2.63125, - "weight": 5.536402758731217e-05 - }, - { - "days": 2.632639, - "weight": 0.0001602642903843247 - }, - { - "days": 2.633333, - "weight": 0.00014132396515708633 - }, - { - "days": 2.636806, - "weight": 0.0004997331963802125 - }, - { - "days": 2.672917, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.674306, - "weight": 5.2450131398506267e-05 - }, - { - "days": 2.680556, - "weight": 0.0002447672798596959 - }, - { - "days": 2.684722, - "weight": 9.907247041940072e-05 - }, - { - "days": 2.686806, - "weight": 7.284740472014759e-05 - }, - { - "days": 2.688194, - "weight": 0.00043271358403767666 - }, - { - "days": 2.689583, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.690278, - "weight": 0.00011801279564663909 - }, - { - "days": 2.69375, - "weight": 3.205285807686494e-05 - }, - { - "days": 2.694444, - "weight": 8.15890932865653e-05 - }, - { - "days": 2.698611, - "weight": 0.0001150988994578332 - }, - { - "days": 2.700694, - "weight": 3.6423702360073794e-05 - }, - { - "days": 2.713889, - "weight": 2.476811760485018e-05 - }, - { - "days": 2.729861, - "weight": 4.953623520970036e-05 - }, - { - "days": 2.731944, - "weight": 2.476811760485018e-05 - }, - { - "days": 2.739583, - "weight": 0.00018066156370596603 - }, - { - "days": 2.747222, - "weight": 0.000632315472970881 - }, - { - "days": 2.748611, - "weight": 6.410571615372988e-05 - }, - { - "days": 2.75, - "weight": 7.284740472014759e-05 - }, - { - "days": 2.750694, - "weight": 4.6622339020894455e-05 - }, - { - "days": 2.751389, - "weight": 4.807928711529741e-05 - }, - { - "days": 2.754861, - "weight": 9.761552232499777e-05 - }, - { - "days": 2.75625, - "weight": 0.00015880734228992173 - }, - { - "days": 2.757639, - "weight": 2.6225065699253133e-05 - }, - { - "days": 2.780556, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.793056, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.794444, - "weight": 4.516539092649151e-05 - }, - { - "days": 2.804861, - "weight": 5.9734871870521025e-05 - }, - { - "days": 2.805556, - "weight": 2.7682013793656084e-05 - }, - { - "days": 2.80625, - "weight": 0.001148075098389526 - }, - { - "days": 2.807639, - "weight": 7.721824900335645e-05 - }, - { - "days": 2.809028, - "weight": 2.6225065699253133e-05 - }, - { - "days": 2.813889, - "weight": 0.00033072721742947005 - }, - { - "days": 2.816667, - "weight": 7.721824900335645e-05 - }, - { - "days": 2.822222, - "weight": 4.079454664328265e-05 - }, - { - "days": 2.848611, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.849306, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.859028, - "weight": 2.7682013793656084e-05 - }, - { - "days": 2.864583, - "weight": 4.079454664328265e-05 - }, - { - "days": 2.867361, - "weight": 6.701961234253578e-05 - }, - { - "days": 2.868056, - "weight": 4.22514947376856e-05 - }, - { - "days": 2.870833, - "weight": 6.847656043693873e-05 - }, - { - "days": 2.871528, - "weight": 0.00015589344610111584 - }, - { - "days": 2.873611, - "weight": 4.079454664328265e-05 - }, - { - "days": 2.875, - "weight": 3.205285807686494e-05 - }, - { - "days": 2.875694, - "weight": 0.0002637076050869343 - }, - { - "days": 2.880556, - "weight": 0.005501436004465546 - }, - { - "days": 2.882639, - "weight": 0.00012821143230745975 - }, - { - "days": 2.890972, - "weight": 5.9734871870521025e-05 - }, - { - "days": 2.895833, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.913194, - "weight": 9.178772994738596e-05 - }, - { - "days": 2.914583, - "weight": 0.00014132396515708633 - }, - { - "days": 2.91875, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.919444, - "weight": 4.516539092649151e-05 - }, - { - "days": 2.922917, - "weight": 0.000151522601817907 - }, - { - "days": 2.925694, - "weight": 0.0002695353974645461 - }, - { - "days": 2.929167, - "weight": 0.00023165474701006934 - }, - { - "days": 2.934028, - "weight": 0.0002141713698772339 - }, - { - "days": 2.9375, - "weight": 5.827792377611807e-05 - }, - { - "days": 2.940972, - "weight": 0.000120926691835445 - }, - { - "days": 2.941667, - "weight": 0.0007809241785999821 - }, - { - "days": 2.942361, - "weight": 0.003467536464679025 - }, - { - "days": 2.95, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.954167, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.957639, - "weight": 7.139045662574464e-05 - }, - { - "days": 2.959028, - "weight": 3.788065045447675e-05 - }, - { - "days": 2.959722, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.960417, - "weight": 3.350980617126789e-05 - }, - { - "days": 2.965278, - "weight": 2.7682013793656084e-05 - }, - { - "days": 2.965972, - "weight": 3.788065045447675e-05 - }, - { - "days": 2.968056, - "weight": 0.00031032994410782873 - }, - { - "days": 2.969444, - "weight": 4.370844283208855e-05 - }, - { - "days": 2.971528, - "weight": 3.059590998246199e-05 - }, - { - "days": 2.972222, - "weight": 0.00037297871216715564 - }, - { - "days": 2.972917, - "weight": 3.788065045447675e-05 - }, - { - "days": 2.974306, - "weight": 9.761552232499777e-05 - }, - { - "days": 2.976389, - "weight": 2.9138961888059035e-05 - }, - { - "days": 2.977083, - "weight": 4.953623520970036e-05 - }, - { - "days": 2.978472, - "weight": 5.6820975681715117e-05 - }, - { - "days": 2.982639, - "weight": 5.6820975681715117e-05 - }, - { - "days": 2.984722, - "weight": 0.0002141713698772339 - }, - { - "days": 2.985417, - "weight": 3.496675426567084e-05 - }, - { - "days": 2.986806, - "weight": 4.079454664328265e-05 - }, - { - "days": 2.9875, - "weight": 0.0003977468297720058 - }, - { - "days": 2.990972, - "weight": 0.0006614544348589401 - }, - { - "days": 2.993056, - "weight": 0.0004997331963802125 - }, - { - "days": 2.995139, - "weight": 0.00046330949402013867 - }, - { - "days": 2.997917, - "weight": 0.00019668799274439848 - }, - { - "days": 3.0, - "weight": 0.0003744356602615586 - }, - { - "days": 3.008333, - "weight": 0.0019683368755383877 - }, - { - "days": 3.011111, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.015278, - "weight": 7.139045662574464e-05 - }, - { - "days": 3.017361, - "weight": 4.953623520970036e-05 - }, - { - "days": 3.030556, - "weight": 5.6820975681715117e-05 - }, - { - "days": 3.031944, - "weight": 4.22514947376856e-05 - }, - { - "days": 3.032639, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.033333, - "weight": 0.00014715175753469813 - }, - { - "days": 3.036111, - "weight": 0.00011655584755223614 - }, - { - "days": 3.0375, - "weight": 4.6622339020894455e-05 - }, - { - "days": 3.038194, - "weight": 0.00025787981270932245 - }, - { - "days": 3.042361, - "weight": 0.0005332430025514804 - }, - { - "days": 3.04375, - "weight": 7.139045662574464e-05 - }, - { - "days": 3.048611, - "weight": 0.0003977468297720058 - }, - { - "days": 3.049306, - "weight": 0.00011364195136343023 - }, - { - "days": 3.050694, - "weight": 0.00027827708603096377 - }, - { - "days": 3.052083, - "weight": 2.6225065699253133e-05 - }, - { - "days": 3.055556, - "weight": 5.6820975681715117e-05 - }, - { - "days": 3.056944, - "weight": 7.57613009089535e-05 - }, - { - "days": 3.059722, - "weight": 0.000332184165523873 - }, - { - "days": 3.061111, - "weight": 0.00018794630417798077 - }, - { - "days": 3.090972, - "weight": 7.57613009089535e-05 - }, - { - "days": 3.095139, - "weight": 2.7682013793656084e-05 - }, - { - "days": 3.098611, - "weight": 3.059590998246199e-05 - }, - { - "days": 3.104167, - "weight": 4.22514947376856e-05 - }, - { - "days": 3.105556, - "weight": 3.496675426567084e-05 - }, - { - "days": 3.106944, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.107639, - "weight": 0.0001544364980067129 - }, - { - "days": 3.109028, - "weight": 4.807928711529741e-05 - }, - { - "days": 3.1125, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.114583, - "weight": 0.00012238363992984796 - }, - { - "days": 3.115278, - "weight": 5.827792377611807e-05 - }, - { - "days": 3.115972, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.116667, - "weight": 6.119181996492398e-05 - }, - { - "days": 3.123611, - "weight": 6.119181996492398e-05 - }, - { - "days": 3.138194, - "weight": 2.7682013793656084e-05 - }, - { - "days": 3.15, - "weight": 3.059590998246199e-05 - }, - { - "days": 3.156944, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.157639, - "weight": 3.788065045447675e-05 - }, - { - "days": 3.165278, - "weight": 6.701961234253578e-05 - }, - { - "days": 3.169444, - "weight": 4.516539092649151e-05 - }, - { - "days": 3.170139, - "weight": 3.788065045447675e-05 - }, - { - "days": 3.174306, - "weight": 3.059590998246199e-05 - }, - { - "days": 3.175, - "weight": 6.556266424813284e-05 - }, - { - "days": 3.177778, - "weight": 2.6225065699253133e-05 - }, - { - "days": 3.18125, - "weight": 0.00011218500326902729 - }, - { - "days": 3.213194, - "weight": 0.00015006565372350402 - }, - { - "days": 3.224306, - "weight": 8.887383375858005e-05 - }, - { - "days": 3.23125, - "weight": 4.953623520970036e-05 - }, - { - "days": 3.233333, - "weight": 3.059590998246199e-05 - }, - { - "days": 3.234028, - "weight": 4.370844283208855e-05 - }, - { - "days": 3.235417, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.236111, - "weight": 2.6225065699253133e-05 - }, - { - "days": 3.275, - "weight": 4.6622339020894455e-05 - }, - { - "days": 3.2875, - "weight": 8.887383375858005e-05 - }, - { - "days": 3.291667, - "weight": 2.7682013793656084e-05 - }, - { - "days": 3.293056, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.297917, - "weight": 2.6225065699253133e-05 - }, - { - "days": 3.300694, - "weight": 0.00010635721089141548 - }, - { - "days": 3.343056, - "weight": 5.3907079492909214e-05 - }, - { - "days": 3.347222, - "weight": 4.370844283208855e-05 - }, - { - "days": 3.349306, - "weight": 4.6622339020894455e-05 - }, - { - "days": 3.359028, - "weight": 3.059590998246199e-05 - }, - { - "days": 3.404167, - "weight": 3.350980617126789e-05 - }, - { - "days": 3.406944, - "weight": 2.7682013793656084e-05 - }, - { - "days": 3.409028, - "weight": 3.205285807686494e-05 - }, - { - "days": 3.417361, - "weight": 8.013214519216235e-05 - }, - { - "days": 3.419444, - "weight": 4.22514947376856e-05 - }, - { - "days": 3.478472, - "weight": 4.079454664328265e-05 - }, - { - "days": 3.479167, - "weight": 3.059590998246199e-05 - }, - { - "days": 3.485417, - "weight": 3.9337598548879697e-05 - }, - { - "days": 3.524306, - "weight": 2.7682013793656084e-05 - }, - { - "days": 3.533333, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.544444, - "weight": 4.370844283208855e-05 - }, - { - "days": 3.545139, - "weight": 7.721824900335645e-05 - }, - { - "days": 3.590278, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.611111, - "weight": 8.013214519216235e-05 - }, - { - "days": 3.660417, - "weight": 3.788065045447675e-05 - }, - { - "days": 3.670139, - "weight": 0.00014569480944029518 - }, - { - "days": 3.719444, - "weight": 4.807928711529741e-05 - }, - { - "days": 3.723611, - "weight": 2.7682013793656084e-05 - }, - { - "days": 3.727778, - "weight": 0.00017191987513954832 - }, - { - "days": 3.757639, - "weight": 3.350980617126789e-05 - }, - { - "days": 3.767361, - "weight": 3.788065045447675e-05 - }, - { - "days": 3.779167, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.782639, - "weight": 2.6225065699253133e-05 - }, - { - "days": 3.784722, - "weight": 3.788065045447675e-05 - }, - { - "days": 3.786111, - "weight": 0.0001937740965555926 - }, - { - "days": 3.798611, - "weight": 2.476811760485018e-05 - }, - { - "days": 3.804861, - "weight": 2.7682013793656084e-05 - }, - { - "days": 3.809028, - "weight": 3.9337598548879697e-05 - }, - { - "days": 3.822222, - "weight": 4.22514947376856e-05 - }, - { - "days": 3.831944, - "weight": 3.6423702360073794e-05 - }, - { - "days": 3.839583, - "weight": 2.7682013793656084e-05 - }, - { - "days": 3.840278, - "weight": 2.9138961888059035e-05 - }, - { - "days": 3.849306, - "weight": 0.0003773495564503645 - }, - { - "days": 3.857639, - "weight": 4.079454664328265e-05 - }, - { - "days": 3.86875, - "weight": 3.205285807686494e-05 - }, - { - "days": 3.884028, - "weight": 3.9337598548879697e-05 - }, - { - "days": 3.89375, - "weight": 4.079454664328265e-05 - }, - { - "days": 3.898611, - "weight": 0.0007386726838622965 - }, - { - "days": 3.899306, - "weight": 2.7682013793656084e-05 - }, - { - "days": 3.927083, - "weight": 7.284740472014759e-05 - }, - { - "days": 3.930556, - "weight": 5.536402758731217e-05 - }, - { - "days": 3.931944, - "weight": 0.00015589344610111584 - }, - { - "days": 3.932639, - "weight": 4.953623520970036e-05 - }, - { - "days": 3.943056, - "weight": 5.9734871870521025e-05 - }, - { - "days": 3.94375, - "weight": 3.205285807686494e-05 - }, - { - "days": 3.945833, - "weight": 5.536402758731217e-05 - }, - { - "days": 3.952083, - "weight": 3.059590998246199e-05 - }, - { - "days": 3.952778, - "weight": 9.907247041940072e-05 - }, - { - "days": 3.953472, - "weight": 4.516539092649151e-05 - }, - { - "days": 3.95625, - "weight": 3.496675426567084e-05 - }, - { - "days": 3.959722, - "weight": 0.00010781415898581843 - }, - { - "days": 3.961111, - "weight": 3.496675426567084e-05 - }, - { - "days": 3.963889, - "weight": 7.139045662574464e-05 - }, - { - "days": 3.965278, - "weight": 0.0003525814388455143 - }, - { - "days": 3.968056, - "weight": 3.205285807686494e-05 - }, - { - "days": 3.984028, - "weight": 3.205285807686494e-05 - }, - { - "days": 3.99375, - "weight": 0.00013986701706268336 - }, - { - "days": 3.995833, - "weight": 7.430435281455054e-05 - }, - { - "days": 3.996528, - "weight": 5.3907079492909214e-05 - }, - { - "days": 3.998611, - "weight": 7.139045662574464e-05 - }, - { - "days": 4.001389, - "weight": 0.00017337682323395127 - }, - { - "days": 4.002778, - "weight": 3.205285807686494e-05 - }, - { - "days": 4.004167, - "weight": 4.079454664328265e-05 - }, - { - "days": 4.00625, - "weight": 3.205285807686494e-05 - }, - { - "days": 4.007639, - "weight": 6.264876805932693e-05 - }, - { - "days": 4.008333, - "weight": 2.9138961888059035e-05 - }, - { - "days": 4.011806, - "weight": 0.00011655584755223614 - }, - { - "days": 4.015278, - "weight": 5.6820975681715117e-05 - }, - { - "days": 4.018056, - "weight": 4.953623520970036e-05 - }, - { - "days": 4.01875, - "weight": 5.6820975681715117e-05 - }, - { - "days": 4.019444, - "weight": 0.00019523104464999554 - }, - { - "days": 4.020833, - "weight": 0.00010052941851380368 - }, - { - "days": 4.021528, - "weight": 4.079454664328265e-05 - }, - { - "days": 4.025, - "weight": 0.00015589344610111584 - }, - { - "days": 4.029861, - "weight": 3.059590998246199e-05 - }, - { - "days": 4.035417, - "weight": 3.350980617126789e-05 - }, - { - "days": 4.047917, - "weight": 4.807928711529741e-05 - }, - { - "days": 4.054167, - "weight": 3.205285807686494e-05 - }, - { - "days": 4.059722, - "weight": 4.6622339020894455e-05 - }, - { - "days": 4.063194, - "weight": 7.284740472014759e-05 - }, - { - "days": 4.069444, - "weight": 2.476811760485018e-05 - }, - { - "days": 4.070139, - "weight": 5.9734871870521025e-05 - }, - { - "days": 4.074306, - "weight": 7.284740472014759e-05 - }, - { - "days": 4.078472, - "weight": 2.7682013793656084e-05 - }, - { - "days": 4.079861, - "weight": 3.496675426567084e-05 - }, - { - "days": 4.080556, - "weight": 2.7682013793656084e-05 - }, - { - "days": 4.082639, - "weight": 0.00010927110708022139 - }, - { - "days": 4.095833, - "weight": 2.6225065699253133e-05 - }, - { - "days": 4.11875, - "weight": 2.7682013793656084e-05 - }, - { - "days": 4.127083, - "weight": 5.2450131398506267e-05 - }, - { - "days": 4.129167, - "weight": 3.496675426567084e-05 - }, - { - "days": 4.131944, - "weight": 4.516539092649151e-05 - }, - { - "days": 4.1375, - "weight": 3.6423702360073794e-05 - }, - { - "days": 4.148611, - "weight": 7.57613009089535e-05 - }, - { - "days": 4.181944, - "weight": 4.22514947376856e-05 - }, - { - "days": 4.209028, - "weight": 2.7682013793656084e-05 - }, - { - "days": 4.211806, - "weight": 3.059590998246199e-05 - }, - { - "days": 4.23125, - "weight": 4.370844283208855e-05 - }, - { - "days": 4.268056, - "weight": 2.6225065699253133e-05 - }, - { - "days": 4.273611, - "weight": 2.9138961888059035e-05 - }, - { - "days": 4.305556, - "weight": 2.6225065699253133e-05 - }, - { - "days": 4.334722, - "weight": 8.595993756977416e-05 - }, - { - "days": 4.438889, - "weight": 3.205285807686494e-05 - }, - { - "days": 4.450694, - "weight": 2.476811760485018e-05 - }, - { - "days": 4.467361, - "weight": 2.9138961888059035e-05 - }, - { - "days": 4.619444, - "weight": 4.370844283208855e-05 - }, - { - "days": 4.731944, - "weight": 2.7682013793656084e-05 - }, - { - "days": 4.760417, - "weight": 2.9138961888059035e-05 - }, - { - "days": 4.770833, - "weight": 0.00017920461561156306 - }, - { - "days": 4.801389, - "weight": 2.7682013793656084e-05 - }, - { - "days": 4.827083, - "weight": 0.00013695312087387746 - }, - { - "days": 4.884722, - "weight": 0.00023311169510447228 - }, - { - "days": 4.886806, - "weight": 2.6225065699253133e-05 - }, - { - "days": 4.900694, - "weight": 3.205285807686494e-05 - }, - { - "days": 4.940278, - "weight": 2.6225065699253133e-05 - }, - { - "days": 4.942361, - "weight": 6.119181996492398e-05 - }, - { - "days": 4.95, - "weight": 8.887383375858005e-05 - }, - { - "days": 4.951389, - "weight": 0.00023311169510447228 - }, - { - "days": 4.952083, - "weight": 2.6225065699253133e-05 - }, - { - "days": 4.952778, - "weight": 2.6225065699253133e-05 - }, - { - "days": 4.963889, - "weight": 3.788065045447675e-05 - }, - { - "days": 4.970139, - "weight": 0.00015006565372350402 - }, - { - "days": 4.978472, - "weight": 3.205285807686494e-05 - }, - { - "days": 4.9875, - "weight": 5.9734871870521025e-05 - }, - { - "days": 4.99375, - "weight": 2.9138961888059035e-05 - }, - { - "days": 5.0, - "weight": 2.476811760485018e-05 - }, - { - "days": 5.00625, - "weight": 3.059590998246199e-05 - }, - { - "days": 5.007639, - "weight": 3.9337598548879697e-05 - }, - { - "days": 5.011806, - "weight": 3.9337598548879697e-05 - }, - { - "days": 5.016667, - "weight": 3.788065045447675e-05 - }, - { - "days": 5.018056, - "weight": 9.470162613619187e-05 - }, - { - "days": 5.019444, - "weight": 6.264876805932693e-05 - }, - { - "days": 5.027778, - "weight": 3.205285807686494e-05 - }, - { - "days": 5.03125, - "weight": 2.9138961888059035e-05 - }, - { - "days": 5.054861, - "weight": 6.556266424813284e-05 - }, - { - "days": 5.078472, - "weight": 4.516539092649151e-05 - }, - { - "days": 5.086111, - "weight": 3.205285807686494e-05 - }, - { - "days": 5.095833, - "weight": 3.496675426567084e-05 - }, - { - "days": 5.097917, - "weight": 2.6225065699253133e-05 - }, - { - "days": 5.103472, - "weight": 5.6820975681715117e-05 - }, - { - "days": 5.114583, - "weight": 3.059590998246199e-05 - }, - { - "days": 5.13125, - "weight": 2.9138961888059035e-05 - }, - { - "days": 5.771528, - "weight": 5.6820975681715117e-05 - }, - { - "days": 5.838194, - "weight": 8.45029894753712e-05 - }, - { - "days": 5.891667, - "weight": 3.496675426567084e-05 - }, - { - "days": 5.91875, - "weight": 0.00013403922468507157 - }, - { - "days": 5.936111, - "weight": 4.370844283208855e-05 - }, - { - "days": 5.947917, - "weight": 2.7682013793656084e-05 - }, - { - "days": 5.986111, - "weight": 6.993350853134168e-05 - }, - { - "days": 5.997222, - "weight": 3.496675426567084e-05 - }, - { - "days": 5.998611, - "weight": 2.9138961888059035e-05 - }, - { - "days": 6.002778, - "weight": 4.370844283208855e-05 - }, - { - "days": 6.00625, - "weight": 3.9337598548879697e-05 - }, - { - "days": 6.017361, - "weight": 3.6423702360073794e-05 - }, - { - "days": 6.04375, - "weight": 5.099318330410331e-05 - }, - { - "days": 6.124306, - "weight": 3.496675426567084e-05 - }, - { - "days": 6.765972, - "weight": 3.350980617126789e-05 - }, - { - "days": 6.844444, - "weight": 6.119181996492398e-05 - }, - { - "days": 6.902083, - "weight": 6.264876805932693e-05 - }, - { - "days": 6.947917, - "weight": 2.9138961888059035e-05 - }, - { - "days": 6.963194, - "weight": 6.556266424813284e-05 - }, - { - "days": 6.970833, - "weight": 3.205285807686494e-05 - }, - { - "days": 7.010417, - "weight": 5.099318330410331e-05 - }, - { - "days": 7.017361, - "weight": 4.22514947376856e-05 - }, - { - "days": 7.018056, - "weight": 2.6225065699253133e-05 - }, - { - "days": 7.051389, - "weight": 3.059590998246199e-05 - }, - { - "days": 7.965278, - "weight": 2.9138961888059035e-05 - }, - { - "days": 9.828472, - "weight": 8.013214519216235e-05 - }, - { - "days": 9.863889, - "weight": 4.22514947376856e-05 - }, - { - "days": 9.931944, - "weight": 8.304604138096825e-05 - }, - { - "days": 9.985417, - "weight": 3.6423702360073794e-05 - }, - { - "days": 10.048611, - "weight": 3.350980617126789e-05 - }, - { - "days": 10.845833, - "weight": 3.205285807686494e-05 - }, - { - "days": 10.914583, - "weight": 2.7682013793656084e-05 - }, - { - "days": 11.076389, - "weight": 5.6820975681715117e-05 - }, - { - "days": 11.150694, - "weight": 2.7682013793656084e-05 - }, - { - "days": 11.209722, - "weight": 4.370844283208855e-05 - }, - { - "days": 11.26875, - "weight": 3.9337598548879697e-05 - }, - { - "days": 11.844444, - "weight": 3.350980617126789e-05 - }, - { - "days": 11.904861, - "weight": 3.205285807686494e-05 - }, - { - "days": 12.906944, - "weight": 3.496675426567084e-05 - }, - { - "days": 16.880556, - "weight": 3.6423702360073794e-05 - }, - { - "days": 17.958333, - "weight": 4.370844283208855e-05 - }, - { - "days": 22.287131, - "weight": 0.008333333333333333 - }, - { - "days": 25.1029, - "weight": 0.008333333333333333 - }, - { - "days": 28.274415, - "weight": 0.008333333333333333 - }, - { - "days": 31.846621, - "weight": 0.008333333333333333 - }, - { - "days": 35.870141, - "weight": 0.008333333333333333 - }, - { - "days": 40.401996, - "weight": 0.008333333333333333 - }, - { - "days": 45.506408, - "weight": 0.008333333333333333 - }, - { - "days": 51.255714, - "weight": 0.008333333333333333 - }, - { - "days": 57.73139, - "weight": 0.008333333333333333 - }, - { - "days": 65.025208, - "weight": 0.008333333333333333 - }, - { - "days": 73.240531, - "weight": 0.008333333333333333 - }, - { - "days": 82.493782, - "weight": 0.008333333333333333 - }, - { - "days": 92.916094, - "weight": 0.008333333333333333 - }, - { - "days": 104.655168, - "weight": 0.008333333333333333 - }, - { - "days": 117.877362, - "weight": 0.008333333333333333 - }, - { - "days": 132.770057, - "weight": 0.008333333333333333 - }, - { - "days": 149.544303, - "weight": 0.008333333333333333 - }, - { - "days": 168.437818, - "weight": 0.008333333333333333 - }, - { - "days": 189.71835, - "weight": 0.008333333333333333 - }, - { - "days": 213.687476, - "weight": 0.008333333333333333 - }, - { - "days": 240.684877, - "weight": 0.008333333333333333 - }, - { - "days": 271.093144, - "weight": 0.008333333333333333 - }, - { - "days": 305.34321, - "weight": 0.008333333333333333 - }, - { - "days": 343.920448, - "weight": 0.008333333333333333 - }, - { - "new_client": true, - "weight": 0.01767569428129661 - } - ] -} \ No newline at end of file diff --git a/tools/DeltaIndexTestTool/dist-tail-30.json b/tools/DeltaIndexTestTool/dist-tail-30.json deleted file mode 100644 index a87a06a847..0000000000 --- a/tools/DeltaIndexTestTool/dist-tail-30.json +++ /dev/null @@ -1,7301 +0,0 @@ -{ - "description": "Telemetry-derived from C:\\Users\\johnmcp\\Downloads\\export.csv: 784,419 download events, 1.55% net-new clients, observed ages 0-18.0 days, plus 30.0% reinstated stale tail out to 344 days", - "buckets": [ - { - "days": 0.0, - "weight": 3.5695228312872314e-05 - }, - { - "days": 0.000694, - "weight": 0.0003786243860329671 - }, - { - "days": 0.028472, - "weight": 0.0015246961807926888 - }, - { - "days": 0.042361, - "weight": 4.0794546643282644e-05 - }, - { - "days": 0.043056, - "weight": 5.736733121711622e-05 - }, - { - "days": 0.047222, - "weight": 0.005720160337137788 - }, - { - "days": 0.049306, - "weight": 3.5695228312872314e-05 - }, - { - "days": 0.050694, - "weight": 0.0024833680269098313 - }, - { - "days": 0.051389, - "weight": 0.006202045919361565 - }, - { - "days": 0.052083, - "weight": 2.5496591652051656e-05 - }, - { - "days": 0.054167, - "weight": 0.00358354595669586 - }, - { - "days": 0.054861, - "weight": 0.005611799822616569 - }, - { - "days": 0.055556, - "weight": 0.004612333429856144 - }, - { - "days": 0.056944, - "weight": 0.004902994574689533 - }, - { - "days": 0.057639, - "weight": 0.006765520594871907 - }, - { - "days": 0.058333, - "weight": 0.003002223667029082 - }, - { - "days": 0.059028, - "weight": 0.03097453436849495 - }, - { - "days": 0.059722, - "weight": 0.014137860071062642 - }, - { - "days": 0.060417, - "weight": 0.005934331707015022 - }, - { - "days": 0.061111, - "weight": 0.012726623723121583 - }, - { - "days": 0.061806, - "weight": 0.014233472289757836 - }, - { - "days": 0.0625, - "weight": 0.00265547002056118 - }, - { - "days": 0.063194, - "weight": 0.009821287104370297 - }, - { - "days": 0.063889, - "weight": 0.010939312648312762 - }, - { - "days": 0.064583, - "weight": 0.0025891788822658456 - }, - { - "days": 0.065278, - "weight": 0.005758405224615866 - }, - { - "days": 0.065972, - "weight": 0.007198962652956785 - }, - { - "days": 0.066667, - "weight": 0.011862289266117033 - }, - { - "days": 0.067361, - "weight": 0.003281411345619048 - }, - { - "days": 0.069444, - "weight": 0.004599585134030118 - }, - { - "days": 0.070833, - "weight": 0.0028097244000560924 - }, - { - "days": 0.072222, - "weight": 0.002972902586629223 - }, - { - "days": 0.072917, - "weight": 0.002608301326004884 - }, - { - "days": 0.074306, - "weight": 0.009084435605626004 - }, - { - "days": 0.076389, - "weight": 4.5893864973692975e-05 - }, - { - "days": 0.078472, - "weight": 0.0036804330049736564 - }, - { - "days": 0.084028, - "weight": 0.0024425734802665483 - }, - { - "days": 0.084722, - "weight": 0.00354402623963518 - }, - { - "days": 0.085417, - "weight": 0.0036090425483479116 - }, - { - "days": 0.0875, - "weight": 0.00016827750490354092 - }, - { - "days": 0.088889, - "weight": 0.0016980730040266402 - }, - { - "days": 0.102083, - "weight": 0.0009127779811434492 - }, - { - "days": 0.109722, - "weight": 0.0008745330936653717 - }, - { - "days": 0.110417, - "weight": 0.001408686688775854 - }, - { - "days": 0.113194, - "weight": 0.0035172548184005258 - }, - { - "days": 0.114583, - "weight": 0.0011294990101858883 - }, - { - "days": 0.115278, - "weight": 5.099318330410331e-05 - }, - { - "days": 0.115972, - "weight": 2.9321080399859404e-05 - }, - { - "days": 0.116667, - "weight": 0.0011447969651771192 - }, - { - "days": 0.117361, - "weight": 0.0022832197824412255 - }, - { - "days": 0.118056, - "weight": 0.0023482360911539573 - }, - { - "days": 0.11875, - "weight": 0.0015119478849666631 - }, - { - "days": 0.119444, - "weight": 0.0011906908301508123 - }, - { - "days": 0.120139, - "weight": 0.0015361696470361122 - }, - { - "days": 0.120833, - "weight": 0.0013398458913153144 - }, - { - "days": 0.121528, - "weight": 0.001358968335054353 - }, - { - "days": 0.122222, - "weight": 0.0012365846951245053 - }, - { - "days": 0.122917, - "weight": 0.0009892677560996042 - }, - { - "days": 0.123611, - "weight": 0.0004742366047281608 - }, - { - "days": 0.124306, - "weight": 0.001402312540862841 - }, - { - "days": 0.125, - "weight": 0.0005214052992844563 - }, - { - "days": 0.125694, - "weight": 0.0032648385610452146 - }, - { - "days": 0.126389, - "weight": 0.000601719562988419 - }, - { - "days": 0.127083, - "weight": 0.001958138238877567 - }, - { - "days": 0.127778, - "weight": 0.001973436193868798 - }, - { - "days": 0.130556, - "weight": 0.00021672102904243907 - }, - { - "days": 0.13125, - "weight": 0.0010262378139950792 - }, - { - "days": 0.131944, - "weight": 0.0005481767205191106 - }, - { - "days": 0.132639, - "weight": 0.0010593833831427462 - }, - { - "days": 0.133333, - "weight": 0.0021072933000420694 - }, - { - "days": 0.134028, - "weight": 0.0016368811840617161 - }, - { - "days": 0.134722, - "weight": 0.0005048325147106227 - }, - { - "days": 0.136111, - "weight": 0.00104153576898631 - }, - { - "days": 0.1375, - "weight": 0.00043471688766748073 - }, - { - "days": 0.138194, - "weight": 0.0003837237043633774 - }, - { - "days": 0.14375, - "weight": 0.0003046842702420173 - }, - { - "days": 0.144444, - "weight": 0.0008987548557348208 - }, - { - "days": 0.147917, - "weight": 0.0008260895695264736 - }, - { - "days": 0.148611, - "weight": 0.00018612511905997708 - }, - { - "days": 0.149306, - "weight": 0.0006998814408488179 - }, - { - "days": 0.150694, - "weight": 0.0009573970165345396 - }, - { - "days": 0.161111, - "weight": 0.0005405277430234951 - }, - { - "days": 0.164583, - "weight": 2.677142123465424e-05 - }, - { - "days": 0.168056, - "weight": 0.0005328787655278796 - }, - { - "days": 0.169444, - "weight": 0.001197064978063825 - }, - { - "days": 0.172222, - "weight": 0.0005328787655278796 - }, - { - "days": 0.172917, - "weight": 4.716869455629556e-05 - }, - { - "days": 0.173611, - "weight": 0.0007916691707962038 - }, - { - "days": 0.175, - "weight": 0.0004028461481024161 - }, - { - "days": 0.175694, - "weight": 0.000980343949021386 - }, - { - "days": 0.176389, - "weight": 0.00045511416098912205 - }, - { - "days": 0.177083, - "weight": 0.0007827453637179858 - }, - { - "days": 0.177778, - "weight": 0.0014431070875061236 - }, - { - "days": 0.178472, - "weight": 0.000470412115980353 - }, - { - "days": 0.179167, - "weight": 0.001708271640687461 - }, - { - "days": 0.179861, - "weight": 0.0007865698524657936 - }, - { - "days": 0.18125, - "weight": 0.0018612511905997707 - }, - { - "days": 0.181944, - "weight": 0.0010122146885864507 - }, - { - "days": 0.182639, - "weight": 0.00040922029601542904 - }, - { - "days": 0.183333, - "weight": 0.0004079454664328265 - }, - { - "days": 0.184028, - "weight": 0.0007738215566397678 - }, - { - "days": 0.184722, - "weight": 0.0010517344056471308 - }, - { - "days": 0.185417, - "weight": 0.0005379780838582899 - }, - { - "days": 0.186111, - "weight": 0.001931366817642913 - }, - { - "days": 0.186806, - "weight": 0.00023201898403367005 - }, - { - "days": 0.1875, - "weight": 0.00033273052105927406 - }, - { - "days": 0.188889, - "weight": 0.0006119181996492397 - }, - { - "days": 0.190972, - "weight": 0.0003747998972851593 - }, - { - "days": 0.192361, - "weight": 0.001093803781873016 - }, - { - "days": 0.194444, - "weight": 0.0004946338780498021 - }, - { - "days": 0.195139, - "weight": 0.002144263357937544 - }, - { - "days": 0.196528, - "weight": 0.0008847317303261925 - }, - { - "days": 0.197222, - "weight": 0.0003097835885724276 - }, - { - "days": 0.197917, - "weight": 0.00020142307405120806 - }, - { - "days": 0.2, - "weight": 0.00027663801942476047 - }, - { - "days": 0.202083, - "weight": 0.0011282241806032857 - }, - { - "days": 0.204167, - "weight": 0.0004729617751455582 - }, - { - "days": 0.208333, - "weight": 0.0005073821738758279 - }, - { - "days": 0.210417, - "weight": 0.0006182923475622526 - }, - { - "days": 0.211111, - "weight": 0.0003786243860329671 - }, - { - "days": 0.2125, - "weight": 0.00046021347931953237 - }, - { - "days": 0.218056, - "weight": 0.00015807886824272026 - }, - { - "days": 0.222917, - "weight": 0.00025496591652051655 - }, - { - "days": 0.227083, - "weight": 0.00024476727985969587 - }, - { - "days": 0.229167, - "weight": 8.286392286916788e-05 - }, - { - "days": 0.23125, - "weight": 0.0005609250163451364 - }, - { - "days": 0.232639, - "weight": 9.306255952998854e-05 - }, - { - "days": 0.233333, - "weight": 7.648977495615496e-05 - }, - { - "days": 0.234722, - "weight": 8.413875245177046e-05 - }, - { - "days": 0.235417, - "weight": 8.158909328656529e-05 - }, - { - "days": 0.236111, - "weight": 0.00021544619945983648 - }, - { - "days": 0.2375, - "weight": 0.00021289654029463132 - }, - { - "days": 0.238194, - "weight": 8.796324119957821e-05 - }, - { - "days": 0.238889, - "weight": 0.00033273052105927406 - }, - { - "days": 0.239583, - "weight": 0.000177201311981759 - }, - { - "days": 0.240278, - "weight": 0.0003250815435636586 - }, - { - "days": 0.240972, - "weight": 0.00034547881688529994 - }, - { - "days": 0.242361, - "weight": 0.00028428699692037597 - }, - { - "days": 0.243056, - "weight": 0.0006310406433882784 - }, - { - "days": 0.24375, - "weight": 0.00043471688766748073 - }, - { - "days": 0.244444, - "weight": 8.923807078218079e-05 - }, - { - "days": 0.245833, - "weight": 0.00018867477822518224 - }, - { - "days": 0.247917, - "weight": 0.00023839313194668296 - }, - { - "days": 0.248611, - "weight": 6.374147913012914e-05 - }, - { - "days": 0.249306, - "weight": 0.00019504892613819515 - }, - { - "days": 0.251389, - "weight": 9.943670744300145e-05 - }, - { - "days": 0.252083, - "weight": 0.0009229766178042699 - }, - { - "days": 0.252778, - "weight": 0.00021544619945983648 - }, - { - "days": 0.253472, - "weight": 0.00036460126062433867 - }, - { - "days": 0.254167, - "weight": 0.0001835754598947719 - }, - { - "days": 0.254861, - "weight": 8.668841161697562e-05 - }, - { - "days": 0.255556, - "weight": 0.00010453602577341178 - }, - { - "days": 0.25625, - "weight": 9.433738911259112e-05 - }, - { - "days": 0.258333, - "weight": 0.00020397273321641325 - }, - { - "days": 0.259028, - "weight": 6.884079746053947e-05 - }, - { - "days": 0.259722, - "weight": 6.119181996492397e-05 - }, - { - "days": 0.261111, - "weight": 0.0001975985853034003 - }, - { - "days": 0.265278, - "weight": 0.00011728432159943762 - }, - { - "days": 0.268056, - "weight": 0.0001555292090775151 - }, - { - "days": 0.271528, - "weight": 0.00034930330563310766 - }, - { - "days": 0.274306, - "weight": 8.286392286916788e-05 - }, - { - "days": 0.275, - "weight": 8.923807078218079e-05 - }, - { - "days": 0.276389, - "weight": 0.00010071153702560403 - }, - { - "days": 0.279861, - "weight": 0.000177201311981759 - }, - { - "days": 0.281944, - "weight": 0.00012493329909505312 - }, - { - "days": 0.284028, - "weight": 3.314556914766715e-05 - }, - { - "days": 0.286806, - "weight": 0.00010198636660820662 - }, - { - "days": 0.288194, - "weight": 6.884079746053947e-05 - }, - { - "days": 0.290278, - "weight": 0.00018612511905997708 - }, - { - "days": 0.292361, - "weight": 8.541358203437304e-05 - }, - { - "days": 0.293056, - "weight": 0.00011091017368642469 - }, - { - "days": 0.295139, - "weight": 0.00010198636660820662 - }, - { - "days": 0.297222, - "weight": 6.119181996492397e-05 - }, - { - "days": 0.297917, - "weight": 0.0003072339294072224 - }, - { - "days": 0.299306, - "weight": 0.0003467536464679025 - }, - { - "days": 0.3, - "weight": 0.00018867477822518224 - }, - { - "days": 0.300694, - "weight": 0.00026006523485092687 - }, - { - "days": 0.301389, - "weight": 0.00016062852740792542 - }, - { - "days": 0.303472, - "weight": 5.736733121711622e-05 - }, - { - "days": 0.304167, - "weight": 0.00026898904192914497 - }, - { - "days": 0.305556, - "weight": 0.00027663801942476047 - }, - { - "days": 0.30625, - "weight": 0.0001542543794949125 - }, - { - "days": 0.306944, - "weight": 0.00012238363992984793 - }, - { - "days": 0.308333, - "weight": 0.00021162171071202872 - }, - { - "days": 0.309722, - "weight": 0.0008911058782392053 - }, - { - "days": 0.310417, - "weight": 0.00010071153702560403 - }, - { - "days": 0.311111, - "weight": 0.0001415060836688867 - }, - { - "days": 0.311806, - "weight": 0.00033910466897228703 - }, - { - "days": 0.3125, - "weight": 0.0002141713698772339 - }, - { - "days": 0.313194, - "weight": 0.00031233324773763274 - }, - { - "days": 0.313889, - "weight": 9.68870482777963e-05 - }, - { - "days": 0.314583, - "weight": 0.00013130744700806603 - }, - { - "days": 0.315972, - "weight": 9.816187786039887e-05 - }, - { - "days": 0.316667, - "weight": 7.648977495615496e-05 - }, - { - "days": 0.317361, - "weight": 9.433738911259112e-05 - }, - { - "days": 0.31875, - "weight": 3.69700578954749e-05 - }, - { - "days": 0.320139, - "weight": 0.00026261489401613206 - }, - { - "days": 0.320833, - "weight": 7.011562704314205e-05 - }, - { - "days": 0.322222, - "weight": 0.00014405574283409185 - }, - { - "days": 0.323611, - "weight": 3.824488747807748e-05 - }, - { - "days": 0.326389, - "weight": 0.0001325822765906686 - }, - { - "days": 0.327778, - "weight": 0.00010708568493861695 - }, - { - "days": 0.33125, - "weight": 0.00015680403866011767 - }, - { - "days": 0.332639, - "weight": 0.0052816189607225 - }, - { - "days": 0.335417, - "weight": 0.000146605401999297 - }, - { - "days": 0.338194, - "weight": 7.39401157909498e-05 - }, - { - "days": 0.340278, - "weight": 0.00012110881034724535 - }, - { - "days": 0.340972, - "weight": 0.00026388972359873465 - }, - { - "days": 0.343056, - "weight": 0.00010198636660820662 - }, - { - "days": 0.34375, - "weight": 0.00012875778784286084 - }, - { - "days": 0.345139, - "weight": 2.4221762069449073e-05 - }, - { - "days": 0.345833, - "weight": 0.00012238363992984793 - }, - { - "days": 0.349306, - "weight": 8.286392286916788e-05 - }, - { - "days": 0.353472, - "weight": 0.00010453602577341178 - }, - { - "days": 0.354861, - "weight": 9.56122186951937e-05 - }, - { - "days": 0.356944, - "weight": 5.991699038232139e-05 - }, - { - "days": 0.357639, - "weight": 0.00011983398076464277 - }, - { - "days": 0.358333, - "weight": 5.226801288670589e-05 - }, - { - "days": 0.359028, - "weight": 0.00046913728639775046 - }, - { - "days": 0.359722, - "weight": 0.00019249926697299 - }, - { - "days": 0.360417, - "weight": 0.00015297954991230992 - }, - { - "days": 0.361111, - "weight": 8.923807078218079e-05 - }, - { - "days": 0.361806, - "weight": 3.824488747807748e-05 - }, - { - "days": 0.3625, - "weight": 0.00023966796152928555 - }, - { - "days": 0.363194, - "weight": 0.00017975097114696418 - }, - { - "days": 0.363889, - "weight": 7.139045662574463e-05 - }, - { - "days": 0.365972, - "weight": 0.00015042989074710476 - }, - { - "days": 0.366667, - "weight": 7.266528620834722e-05 - }, - { - "days": 0.367361, - "weight": 5.354284246930848e-05 - }, - { - "days": 0.36875, - "weight": 8.923807078218079e-05 - }, - { - "days": 0.369444, - "weight": 0.00011091017368642469 - }, - { - "days": 0.370139, - "weight": 0.00021162171071202872 - }, - { - "days": 0.371528, - "weight": 0.0003314556914766715 - }, - { - "days": 0.372222, - "weight": 0.00012620812867765568 - }, - { - "days": 0.372917, - "weight": 0.00017592648239915642 - }, - { - "days": 0.373611, - "weight": 0.00015680403866011767 - }, - { - "days": 0.375, - "weight": 0.0012824785600981982 - }, - { - "days": 0.376389, - "weight": 0.0003021346110768121 - }, - { - "days": 0.377083, - "weight": 0.00012110881034724535 - }, - { - "days": 0.377778, - "weight": 0.00014278091325148925 - }, - { - "days": 0.378472, - "weight": 0.00012620812867765568 - }, - { - "days": 0.379861, - "weight": 0.00010198636660820662 - }, - { - "days": 0.38125, - "weight": 0.00010708568493861695 - }, - { - "days": 0.3875, - "weight": 0.00010071153702560403 - }, - { - "days": 0.388889, - "weight": 3.9519717060680065e-05 - }, - { - "days": 0.390278, - "weight": 9.051290036478337e-05 - }, - { - "days": 0.392361, - "weight": 9.051290036478337e-05 - }, - { - "days": 0.397222, - "weight": 0.00016445301615573317 - }, - { - "days": 0.398611, - "weight": 0.00018612511905997708 - }, - { - "days": 0.399306, - "weight": 0.00010198636660820662 - }, - { - "days": 0.400694, - "weight": 0.00016700267532093833 - }, - { - "days": 0.404167, - "weight": 0.0001032611961908092 - }, - { - "days": 0.406944, - "weight": 0.0014533057241669443 - }, - { - "days": 0.407639, - "weight": 0.00012748295826025828 - }, - { - "days": 0.409028, - "weight": 9.816187786039887e-05 - }, - { - "days": 0.4125, - "weight": 0.0002065223923816184 - }, - { - "days": 0.414583, - "weight": 5.609250163451364e-05 - }, - { - "days": 0.415972, - "weight": 6.884079746053947e-05 - }, - { - "days": 0.418056, - "weight": 0.00021289654029463132 - }, - { - "days": 0.41875, - "weight": 8.413875245177046e-05 - }, - { - "days": 0.419444, - "weight": 0.00016700267532093833 - }, - { - "days": 0.420139, - "weight": 0.0002422176206944907 - }, - { - "days": 0.421528, - "weight": 0.0003314556914766715 - }, - { - "days": 0.422222, - "weight": 0.00016572784573833576 - }, - { - "days": 0.422917, - "weight": 6.756596787793689e-05 - }, - { - "days": 0.424306, - "weight": 0.00017082716406874608 - }, - { - "days": 0.425, - "weight": 0.00024094279111188814 - }, - { - "days": 0.426389, - "weight": 9.56122186951937e-05 - }, - { - "days": 0.427083, - "weight": 0.00016700267532093833 - }, - { - "days": 0.427778, - "weight": 0.00021289654029463132 - }, - { - "days": 0.429861, - "weight": 0.0012314853767940949 - }, - { - "days": 0.43125, - "weight": 3.824488747807748e-05 - }, - { - "days": 0.431944, - "weight": 0.0001389564245036815 - }, - { - "days": 0.432639, - "weight": 0.00016955233448614351 - }, - { - "days": 0.434028, - "weight": 0.0001491550611645022 - }, - { - "days": 0.435417, - "weight": 0.0001032611961908092 - }, - { - "days": 0.436806, - "weight": 3.4420398730269734e-05 - }, - { - "days": 0.4375, - "weight": 0.0005787726305015726 - }, - { - "days": 0.438194, - "weight": 0.00017210199365134867 - }, - { - "days": 0.438889, - "weight": 0.00021162171071202872 - }, - { - "days": 0.440278, - "weight": 0.00011473466243423244 - }, - { - "days": 0.442361, - "weight": 0.00013130744700806603 - }, - { - "days": 0.443056, - "weight": 0.00011345983285162986 - }, - { - "days": 0.446528, - "weight": 9.178772994738595e-05 - }, - { - "days": 0.447222, - "weight": 0.00026134006443352946 - }, - { - "days": 0.447917, - "weight": 0.00015935369782532285 - }, - { - "days": 0.449306, - "weight": 0.00011983398076464277 - }, - { - "days": 0.453472, - "weight": 0.0002906611448333889 - }, - { - "days": 0.457639, - "weight": 0.0002804625081725682 - }, - { - "days": 0.458333, - "weight": 0.00033910466897228703 - }, - { - "days": 0.459028, - "weight": 6.119181996492397e-05 - }, - { - "days": 0.460417, - "weight": 0.00017082716406874608 - }, - { - "days": 0.4625, - "weight": 0.0007597984312311393 - }, - { - "days": 0.466667, - "weight": 0.0015438186245317277 - }, - { - "days": 0.468056, - "weight": 0.00033400535064187666 - }, - { - "days": 0.470833, - "weight": 3.5695228312872314e-05 - }, - { - "days": 0.472222, - "weight": 0.00015042989074710476 - }, - { - "days": 0.472917, - "weight": 0.00013640676533847635 - }, - { - "days": 0.473611, - "weight": 0.0003747998972851593 - }, - { - "days": 0.474306, - "weight": 0.0004066706368502239 - }, - { - "days": 0.475, - "weight": 0.00022182034737284939 - }, - { - "days": 0.477083, - "weight": 0.0007368514987442928 - }, - { - "days": 0.477778, - "weight": 0.00024731693902490105 - }, - { - "days": 0.479167, - "weight": 0.00035057813521571025 - }, - { - "days": 0.480556, - "weight": 0.0003837237043633774 - }, - { - "days": 0.48125, - "weight": 0.00042196859184145486 - }, - { - "days": 0.483333, - "weight": 0.00033018086189406893 - }, - { - "days": 0.484028, - "weight": 0.001023688154829874 - }, - { - "days": 0.484722, - "weight": 0.0002294693248684649 - }, - { - "days": 0.485417, - "weight": 0.00022182034737284939 - }, - { - "days": 0.486806, - "weight": 0.0009242514473868725 - }, - { - "days": 0.4875, - "weight": 0.00041559444392844195 - }, - { - "days": 0.488194, - "weight": 0.00026388972359873465 - }, - { - "days": 0.488889, - "weight": 0.00023201898403367005 - }, - { - "days": 0.490278, - "weight": 0.0006068188813188294 - }, - { - "days": 0.490972, - "weight": 0.000177201311981759 - }, - { - "days": 0.491667, - "weight": 0.0003097835885724276 - }, - { - "days": 0.492361, - "weight": 0.0001975985853034003 - }, - { - "days": 0.493056, - "weight": 0.0006068188813188294 - }, - { - "days": 0.49375, - "weight": 0.0004908093893019943 - }, - { - "days": 0.494444, - "weight": 0.00269498973762186 - }, - { - "days": 0.495833, - "weight": 0.0002970352927464018 - }, - { - "days": 0.498611, - "weight": 0.0005736733121711622 - }, - { - "days": 0.499306, - "weight": 0.00020269790363381066 - }, - { - "days": 0.500694, - "weight": 0.00022564483612065714 - }, - { - "days": 0.502083, - "weight": 0.00024731693902490105 - }, - { - "days": 0.50625, - "weight": 0.00028428699692037597 - }, - { - "days": 0.506944, - "weight": 0.0005825971192493804 - }, - { - "days": 0.509028, - "weight": 0.00028428699692037597 - }, - { - "days": 0.511111, - "weight": 0.0008490365020133201 - }, - { - "days": 0.5125, - "weight": 0.00010708568493861695 - }, - { - "days": 0.517361, - "weight": 0.0003467536464679025 - }, - { - "days": 0.51875, - "weight": 0.00020397273321641325 - }, - { - "days": 0.521528, - "weight": 0.0004206937622588523 - }, - { - "days": 0.522917, - "weight": 0.00020779722196422097 - }, - { - "days": 0.524306, - "weight": 0.0003008597814942095 - }, - { - "days": 0.525, - "weight": 0.0001402312540862841 - }, - { - "days": 0.526389, - "weight": 0.00019504892613819515 - }, - { - "days": 0.527083, - "weight": 0.0001415060836688867 - }, - { - "days": 0.532639, - "weight": 0.00031360807732023533 - }, - { - "days": 0.533333, - "weight": 0.0005634746755103416 - }, - { - "days": 0.535417, - "weight": 0.0003467536464679025 - }, - { - "days": 0.536111, - "weight": 0.0002371183023640804 - }, - { - "days": 0.536806, - "weight": 0.0010491847464819256 - }, - { - "days": 0.538194, - "weight": 6.62911382953343e-05 - }, - { - "days": 0.539583, - "weight": 0.00044109103558049364 - }, - { - "days": 0.540278, - "weight": 0.0004206937622588523 - }, - { - "days": 0.543056, - "weight": 0.00035695228312872316 - }, - { - "days": 0.544444, - "weight": 0.0006603617237881379 - }, - { - "days": 0.545833, - "weight": 0.0002855618265029785 - }, - { - "days": 0.546528, - "weight": 0.0005494515501017132 - }, - { - "days": 0.547222, - "weight": 0.00024859176860750364 - }, - { - "days": 0.548611, - "weight": 0.00015680403866011767 - }, - { - "days": 0.549306, - "weight": 0.00023456864319887523 - }, - { - "days": 0.55, - "weight": 0.0011167507143598624 - }, - { - "days": 0.550694, - "weight": 0.0007419508170747032 - }, - { - "days": 0.551389, - "weight": 0.00032125705481585084 - }, - { - "days": 0.552083, - "weight": 7.521494537355238e-05 - }, - { - "days": 0.552778, - "weight": 0.00015170472032970735 - }, - { - "days": 0.553472, - "weight": 0.0003939223410241981 - }, - { - "days": 0.554167, - "weight": 4.716869455629556e-05 - }, - { - "days": 0.554861, - "weight": 0.00020269790363381066 - }, - { - "days": 0.555556, - "weight": 0.00017975097114696418 - }, - { - "days": 0.556944, - "weight": 0.00013513193575587378 - }, - { - "days": 0.557639, - "weight": 0.00022691966570325973 - }, - { - "days": 0.558333, - "weight": 0.0002511414277727088 - }, - { - "days": 0.559722, - "weight": 0.000308508758989825 - }, - { - "days": 0.560417, - "weight": 8.031426370396271e-05 - }, - { - "days": 0.568056, - "weight": 0.00010581085535601436 - }, - { - "days": 0.570139, - "weight": 0.00041049512559803163 - }, - { - "days": 0.570833, - "weight": 6.62911382953343e-05 - }, - { - "days": 0.572917, - "weight": 0.00011473466243423244 - }, - { - "days": 0.575, - "weight": 0.00012875778784286084 - }, - { - "days": 0.58125, - "weight": 0.00018102580072956674 - }, - { - "days": 0.584722, - "weight": 0.0004882597301367892 - }, - { - "days": 0.5875, - "weight": 2.804625081725682e-05 - }, - { - "days": 0.590278, - "weight": 5.609250163451364e-05 - }, - { - "days": 0.591667, - "weight": 0.0002830121673377734 - }, - { - "days": 0.592361, - "weight": 0.000161903356990528 - }, - { - "days": 0.593056, - "weight": 6.501630871273172e-05 - }, - { - "days": 0.59375, - "weight": 8.413875245177046e-05 - }, - { - "days": 0.595833, - "weight": 9.051290036478337e-05 - }, - { - "days": 0.597222, - "weight": 0.0001912244373903874 - }, - { - "days": 0.597917, - "weight": 0.0008477616724307175 - }, - { - "days": 0.598611, - "weight": 0.0002995849519116069 - }, - { - "days": 0.599306, - "weight": 6.756596787793689e-05 - }, - { - "days": 0.6, - "weight": 9.68870482777963e-05 - }, - { - "days": 0.601389, - "weight": 0.00021162171071202872 - }, - { - "days": 0.602083, - "weight": 4.334420580848781e-05 - }, - { - "days": 0.602778, - "weight": 0.00015297954991230992 - }, - { - "days": 0.603472, - "weight": 0.00015935369782532285 - }, - { - "days": 0.605556, - "weight": 0.00033910466897228703 - }, - { - "days": 0.606944, - "weight": 0.00012748295826025828 - }, - { - "days": 0.607639, - "weight": 0.00011091017368642469 - }, - { - "days": 0.609028, - "weight": 0.00011600949201683502 - }, - { - "days": 0.609722, - "weight": 0.00018230063031216933 - }, - { - "days": 0.610417, - "weight": 9.68870482777963e-05 - }, - { - "days": 0.611111, - "weight": 0.001325822765906686 - }, - { - "days": 0.611806, - "weight": 0.00023201898403367005 - }, - { - "days": 0.6125, - "weight": 4.9718353721500726e-05 - }, - { - "days": 0.613194, - "weight": 3.824488747807748e-05 - }, - { - "days": 0.613889, - "weight": 0.00022054551779024682 - }, - { - "days": 0.614583, - "weight": 0.0002001482444686055 - }, - { - "days": 0.615278, - "weight": 5.609250163451364e-05 - }, - { - "days": 0.615972, - "weight": 5.609250163451364e-05 - }, - { - "days": 0.616667, - "weight": 0.00026134006443352946 - }, - { - "days": 0.617361, - "weight": 0.00016062852740792542 - }, - { - "days": 0.61875, - "weight": 0.0001389564245036815 - }, - { - "days": 0.619444, - "weight": 6.374147913012914e-05 - }, - { - "days": 0.620139, - "weight": 0.00023074415445106748 - }, - { - "days": 0.622222, - "weight": 5.991699038232139e-05 - }, - { - "days": 0.623611, - "weight": 7.266528620834722e-05 - }, - { - "days": 0.624306, - "weight": 8.413875245177046e-05 - }, - { - "days": 0.628472, - "weight": 5.991699038232139e-05 - }, - { - "days": 0.631944, - "weight": 0.0001912244373903874 - }, - { - "days": 0.632639, - "weight": 6.119181996492397e-05 - }, - { - "days": 0.634028, - "weight": 0.0003187073956506457 - }, - { - "days": 0.642361, - "weight": 8.031426370396271e-05 - }, - { - "days": 0.644444, - "weight": 0.0003875481931111852 - }, - { - "days": 0.648611, - "weight": 8.286392286916788e-05 - }, - { - "days": 0.652778, - "weight": 0.0011702935568291709 - }, - { - "days": 0.654167, - "weight": 0.00032763120272886375 - }, - { - "days": 0.654861, - "weight": 9.943670744300145e-05 - }, - { - "days": 0.655556, - "weight": 0.0002830121673377734 - }, - { - "days": 0.65625, - "weight": 4.206937622588523e-05 - }, - { - "days": 0.656944, - "weight": 5.736733121711622e-05 - }, - { - "days": 0.657639, - "weight": 3.69700578954749e-05 - }, - { - "days": 0.658333, - "weight": 0.00015170472032970735 - }, - { - "days": 0.659028, - "weight": 5.226801288670589e-05 - }, - { - "days": 0.660417, - "weight": 0.00016827750490354092 - }, - { - "days": 0.661806, - "weight": 0.00012620812867765568 - }, - { - "days": 0.6625, - "weight": 5.354284246930848e-05 - }, - { - "days": 0.663194, - "weight": 7.776460453875755e-05 - }, - { - "days": 0.663889, - "weight": 4.206937622588523e-05 - }, - { - "days": 0.664583, - "weight": 0.0006246664954752655 - }, - { - "days": 0.665278, - "weight": 9.051290036478337e-05 - }, - { - "days": 0.665972, - "weight": 0.00012620812867765568 - }, - { - "days": 0.666667, - "weight": 8.158909328656529e-05 - }, - { - "days": 0.667361, - "weight": 0.00011091017368642469 - }, - { - "days": 0.668056, - "weight": 2.9321080399859404e-05 - }, - { - "days": 0.66875, - "weight": 0.0007330270099964851 - }, - { - "days": 0.669444, - "weight": 0.00015170472032970735 - }, - { - "days": 0.670139, - "weight": 0.001185591511820402 - }, - { - "days": 0.670833, - "weight": 0.00011091017368642469 - }, - { - "days": 0.671528, - "weight": 9.943670744300145e-05 - }, - { - "days": 0.672917, - "weight": 0.00017337682323395124 - }, - { - "days": 0.673611, - "weight": 0.00015042989074710476 - }, - { - "days": 0.674306, - "weight": 0.0001185591511820402 - }, - { - "days": 0.675, - "weight": 5.481767205191106e-05 - }, - { - "days": 0.675694, - "weight": 0.00017592648239915642 - }, - { - "days": 0.676389, - "weight": 0.00013640676533847635 - }, - { - "days": 0.677083, - "weight": 0.00016317818657313058 - }, - { - "days": 0.678472, - "weight": 7.011562704314205e-05 - }, - { - "days": 0.68125, - "weight": 4.206937622588523e-05 - }, - { - "days": 0.681944, - "weight": 0.0001975985853034003 - }, - { - "days": 0.682639, - "weight": 8.923807078218079e-05 - }, - { - "days": 0.683333, - "weight": 0.0002740883602595553 - }, - { - "days": 0.684028, - "weight": 0.0003161577364854405 - }, - { - "days": 0.6875, - "weight": 9.433738911259112e-05 - }, - { - "days": 0.688889, - "weight": 0.00012620812867765568 - }, - { - "days": 0.690278, - "weight": 3.9519717060680065e-05 - }, - { - "days": 0.69375, - "weight": 7.266528620834722e-05 - }, - { - "days": 0.695833, - "weight": 4.844352413889815e-05 - }, - { - "days": 0.698611, - "weight": 0.0006335903025534836 - }, - { - "days": 0.70625, - "weight": 2.2946932486846487e-05 - }, - { - "days": 0.707639, - "weight": 8.031426370396271e-05 - }, - { - "days": 0.713889, - "weight": 0.00030595909982461983 - }, - { - "days": 0.716667, - "weight": 0.00011345983285162986 - }, - { - "days": 0.71875, - "weight": 0.000592795755910201 - }, - { - "days": 0.719444, - "weight": 0.0005456270613539054 - }, - { - "days": 0.720139, - "weight": 0.0010759561677165799 - }, - { - "days": 0.720833, - "weight": 0.0002740883602595553 - }, - { - "days": 0.721528, - "weight": 0.00014405574283409185 - }, - { - "days": 0.722222, - "weight": 6.884079746053947e-05 - }, - { - "days": 0.723611, - "weight": 0.00015042989074710476 - }, - { - "days": 0.725, - "weight": 6.374147913012914e-05 - }, - { - "days": 0.725694, - "weight": 8.541358203437304e-05 - }, - { - "days": 0.726389, - "weight": 0.0002651645531813372 - }, - { - "days": 0.727083, - "weight": 8.031426370396271e-05 - }, - { - "days": 0.727778, - "weight": 0.00010963534410382211 - }, - { - "days": 0.728472, - "weight": 0.00016827750490354092 - }, - { - "days": 0.729167, - "weight": 0.00021799585862504163 - }, - { - "days": 0.729861, - "weight": 7.903943412136013e-05 - }, - { - "days": 0.730556, - "weight": 0.00096249633486495 - }, - { - "days": 0.73125, - "weight": 7.011562704314205e-05 - }, - { - "days": 0.731944, - "weight": 5.354284246930848e-05 - }, - { - "days": 0.732639, - "weight": 9.816187786039887e-05 - }, - { - "days": 0.733333, - "weight": 5.736733121711622e-05 - }, - { - "days": 0.734028, - "weight": 0.0001835754598947719 - }, - { - "days": 0.734722, - "weight": 6.501630871273172e-05 - }, - { - "days": 0.735417, - "weight": 0.00024986659819010624 - }, - { - "days": 0.736806, - "weight": 7.39401157909498e-05 - }, - { - "days": 0.7375, - "weight": 0.00010708568493861695 - }, - { - "days": 0.738194, - "weight": 0.000501008025962815 - }, - { - "days": 0.738889, - "weight": 0.0001937740965555926 - }, - { - "days": 0.740278, - "weight": 0.0001032611961908092 - }, - { - "days": 0.740972, - "weight": 4.4619035391090396e-05 - }, - { - "days": 0.741667, - "weight": 7.903943412136013e-05 - }, - { - "days": 0.743056, - "weight": 3.059590998246198e-05 - }, - { - "days": 0.744444, - "weight": 7.521494537355238e-05 - }, - { - "days": 0.746528, - "weight": 7.266528620834722e-05 - }, - { - "days": 0.747222, - "weight": 0.0003939223410241981 - }, - { - "days": 0.748611, - "weight": 9.56122186951937e-05 - }, - { - "days": 0.749306, - "weight": 0.00010581085535601436 - }, - { - "days": 0.75, - "weight": 4.4619035391090396e-05 - }, - { - "days": 0.750694, - "weight": 8.541358203437304e-05 - }, - { - "days": 0.7625, - "weight": 5.099318330410331e-05 - }, - { - "days": 0.776389, - "weight": 0.00016700267532093833 - }, - { - "days": 0.777083, - "weight": 4.5893864973692975e-05 - }, - { - "days": 0.777778, - "weight": 4.206937622588523e-05 - }, - { - "days": 0.778472, - "weight": 0.0003709754085373516 - }, - { - "days": 0.779167, - "weight": 0.0001415060836688867 - }, - { - "days": 0.779861, - "weight": 0.0017656389719045771 - }, - { - "days": 0.780556, - "weight": 0.00032890603231146634 - }, - { - "days": 0.78125, - "weight": 8.031426370396271e-05 - }, - { - "days": 0.781944, - "weight": 7.139045662574463e-05 - }, - { - "days": 0.782639, - "weight": 0.0002434924502770933 - }, - { - "days": 0.783333, - "weight": 6.884079746053947e-05 - }, - { - "days": 0.784028, - "weight": 0.00015042989074710476 - }, - { - "days": 0.785417, - "weight": 0.0003314556914766715 - }, - { - "days": 0.786806, - "weight": 0.00022182034737284939 - }, - { - "days": 0.7875, - "weight": 0.0020295286955033117 - }, - { - "days": 0.788194, - "weight": 0.00043216722850227554 - }, - { - "days": 0.788889, - "weight": 3.314556914766715e-05 - }, - { - "days": 0.789583, - "weight": 0.0005723984825885596 - }, - { - "days": 0.790972, - "weight": 5.354284246930848e-05 - }, - { - "days": 0.791667, - "weight": 0.00025496591652051655 - }, - { - "days": 0.792361, - "weight": 0.00022437000653805457 - }, - { - "days": 0.793056, - "weight": 7.139045662574463e-05 - }, - { - "days": 0.79375, - "weight": 9.943670744300145e-05 - }, - { - "days": 0.795833, - "weight": 0.00031998222523324824 - }, - { - "days": 0.796528, - "weight": 0.00011345983285162986 - }, - { - "days": 0.797917, - "weight": 0.0001338571061732712 - }, - { - "days": 0.798611, - "weight": 0.00027663801942476047 - }, - { - "days": 0.799306, - "weight": 7.648977495615496e-05 - }, - { - "days": 0.8, - "weight": 0.00014278091325148925 - }, - { - "days": 0.800694, - "weight": 4.844352413889815e-05 - }, - { - "days": 0.801389, - "weight": 0.0002881114856681837 - }, - { - "days": 0.802778, - "weight": 6.756596787793689e-05 - }, - { - "days": 0.804167, - "weight": 8.541358203437304e-05 - }, - { - "days": 0.804861, - "weight": 0.00026134006443352946 - }, - { - "days": 0.805556, - "weight": 6.374147913012914e-05 - }, - { - "days": 0.80625, - "weight": 4.5893864973692975e-05 - }, - { - "days": 0.808333, - "weight": 7.139045662574463e-05 - }, - { - "days": 0.809028, - "weight": 7.266528620834722e-05 - }, - { - "days": 0.810417, - "weight": 0.00027663801942476047 - }, - { - "days": 0.811111, - "weight": 0.00021162171071202872 - }, - { - "days": 0.827083, - "weight": 7.776460453875755e-05 - }, - { - "days": 0.832639, - "weight": 0.0001325822765906686 - }, - { - "days": 0.836806, - "weight": 5.864216079971881e-05 - }, - { - "days": 0.838194, - "weight": 0.0002651645531813372 - }, - { - "days": 0.838889, - "weight": 0.0006833086562749843 - }, - { - "days": 0.839583, - "weight": 0.00037225023811995417 - }, - { - "days": 0.840278, - "weight": 7.139045662574463e-05 - }, - { - "days": 0.840972, - "weight": 4.0794546643282644e-05 - }, - { - "days": 0.841667, - "weight": 0.00017465165281655383 - }, - { - "days": 0.842361, - "weight": 8.158909328656529e-05 - }, - { - "days": 0.84375, - "weight": 0.00026261489401613206 - }, - { - "days": 0.844444, - "weight": 0.00011600949201683502 - }, - { - "days": 0.845833, - "weight": 0.00020142307405120806 - }, - { - "days": 0.846528, - "weight": 0.000546901890936508 - }, - { - "days": 0.847222, - "weight": 0.0007266528620834721 - }, - { - "days": 0.849306, - "weight": 0.002183783074998224 - }, - { - "days": 0.85, - "weight": 0.0001338571061732712 - }, - { - "days": 0.850694, - "weight": 0.0003786243860329671 - }, - { - "days": 0.851389, - "weight": 0.000177201311981759 - }, - { - "days": 0.852778, - "weight": 5.864216079971881e-05 - }, - { - "days": 0.854167, - "weight": 6.756596787793689e-05 - }, - { - "days": 0.854861, - "weight": 0.00042579308058926263 - }, - { - "days": 0.855556, - "weight": 0.0001848502894773745 - }, - { - "days": 0.856944, - "weight": 0.00022437000653805457 - }, - { - "days": 0.857639, - "weight": 8.413875245177046e-05 - }, - { - "days": 0.858333, - "weight": 7.648977495615496e-05 - }, - { - "days": 0.860417, - "weight": 0.0001835754598947719 - }, - { - "days": 0.861111, - "weight": 0.0003633264310417361 - }, - { - "days": 0.861806, - "weight": 0.00010836051452121953 - }, - { - "days": 0.863889, - "weight": 0.0003008597814942095 - }, - { - "days": 0.864583, - "weight": 0.000146605401999297 - }, - { - "days": 0.865278, - "weight": 0.0002371183023640804 - }, - { - "days": 0.865972, - "weight": 0.00013768159492107894 - }, - { - "days": 0.866667, - "weight": 9.943670744300145e-05 - }, - { - "days": 0.868056, - "weight": 0.00012875778784286084 - }, - { - "days": 0.86875, - "weight": 0.0002358434727814778 - }, - { - "days": 0.870139, - "weight": 0.00021162171071202872 - }, - { - "days": 0.870833, - "weight": 0.00018994960780778484 - }, - { - "days": 0.872222, - "weight": 0.00018230063031216933 - }, - { - "days": 0.882639, - "weight": 8.541358203437304e-05 - }, - { - "days": 0.890278, - "weight": 4.4619035391090396e-05 - }, - { - "days": 0.891667, - "weight": 0.00025369108693791396 - }, - { - "days": 0.896528, - "weight": 0.000369700578954749 - }, - { - "days": 0.897917, - "weight": 8.413875245177046e-05 - }, - { - "days": 0.898611, - "weight": 0.00033400535064187666 - }, - { - "days": 0.901389, - "weight": 0.0001937740965555926 - }, - { - "days": 0.902083, - "weight": 8.668841161697562e-05 - }, - { - "days": 0.902778, - "weight": 0.00017847614156436158 - }, - { - "days": 0.903472, - "weight": 0.00016955233448614351 - }, - { - "days": 0.904861, - "weight": 0.00026261489401613206 - }, - { - "days": 0.905556, - "weight": 0.0028326713325429386 - }, - { - "days": 0.90625, - "weight": 0.00044619035391090396 - }, - { - "days": 0.906944, - "weight": 0.00046021347931953237 - }, - { - "days": 0.907639, - "weight": 0.00031998222523324824 - }, - { - "days": 0.908333, - "weight": 0.0003607767718765309 - }, - { - "days": 0.909722, - "weight": 5.864216079971881e-05 - }, - { - "days": 0.913194, - "weight": 0.00014533057241669444 - }, - { - "days": 0.913889, - "weight": 0.00017465165281655383 - }, - { - "days": 0.914583, - "weight": 0.0003110584181550302 - }, - { - "days": 0.915278, - "weight": 8.796324119957821e-05 - }, - { - "days": 0.915972, - "weight": 0.0006450637687969069 - }, - { - "days": 0.916667, - "weight": 0.00031233324773763274 - }, - { - "days": 0.917361, - "weight": 2.677142123465424e-05 - }, - { - "days": 0.918056, - "weight": 0.00015935369782532285 - }, - { - "days": 0.921528, - "weight": 0.008364156891455546 - }, - { - "days": 0.922222, - "weight": 0.00016700267532093833 - }, - { - "days": 0.922917, - "weight": 0.0006743848491967663 - }, - { - "days": 0.923611, - "weight": 0.00023329381361627264 - }, - { - "days": 0.924306, - "weight": 0.0019453899430515412 - }, - { - "days": 0.925, - "weight": 0.00015297954991230992 - }, - { - "days": 0.925694, - "weight": 0.0003607767718765309 - }, - { - "days": 0.926389, - "weight": 7.776460453875755e-05 - }, - { - "days": 0.927083, - "weight": 0.0001491550611645022 - }, - { - "days": 0.930556, - "weight": 0.0004206937622588523 - }, - { - "days": 0.93125, - "weight": 0.00024859176860750364 - }, - { - "days": 0.931944, - "weight": 0.00013768159492107894 - }, - { - "days": 0.932639, - "weight": 0.00021672102904243907 - }, - { - "days": 0.933333, - "weight": 0.00024859176860750364 - }, - { - "days": 0.94375, - "weight": 0.0003314556914766715 - }, - { - "days": 0.948611, - "weight": 0.00016955233448614351 - }, - { - "days": 0.950694, - "weight": 0.0002294693248684649 - }, - { - "days": 0.951389, - "weight": 0.0009102283219782441 - }, - { - "days": 0.955556, - "weight": 0.0005800474600841752 - }, - { - "days": 0.95625, - "weight": 0.002067773582981389 - }, - { - "days": 0.956944, - "weight": 0.0007113549070922411 - }, - { - "days": 0.957639, - "weight": 0.0002434924502770933 - }, - { - "days": 0.958333, - "weight": 0.00016572784573833576 - }, - { - "days": 0.959028, - "weight": 0.0004206937622588523 - }, - { - "days": 0.959722, - "weight": 4.844352413889815e-05 - }, - { - "days": 0.960417, - "weight": 0.002545834676457358 - }, - { - "days": 0.961111, - "weight": 0.0012735547530199802 - }, - { - "days": 0.961806, - "weight": 0.00016700267532093833 - }, - { - "days": 0.9625, - "weight": 0.0009777942898561809 - }, - { - "days": 0.964583, - "weight": 0.000794218829961409 - }, - { - "days": 0.965278, - "weight": 0.0003671509197895438 - }, - { - "days": 0.965972, - "weight": 0.0011103765664468497 - }, - { - "days": 0.968056, - "weight": 0.0006386896208838939 - }, - { - "days": 0.96875, - "weight": 0.005013904748375958 - }, - { - "days": 0.970139, - "weight": 0.00034802847605050507 - }, - { - "days": 0.972222, - "weight": 0.0012633561163591595 - }, - { - "days": 0.972917, - "weight": 0.006152327565640064 - }, - { - "days": 0.973611, - "weight": 0.0010912541227078108 - }, - { - "days": 0.974306, - "weight": 0.001133323498933696 - }, - { - "days": 0.975, - "weight": 0.0005443522317713028 - }, - { - "days": 0.975694, - "weight": 0.00017847614156436158 - }, - { - "days": 0.976389, - "weight": 0.0006705603604489585 - }, - { - "days": 0.977083, - "weight": 0.0028097244000560924 - }, - { - "days": 0.977778, - "weight": 3.314556914766715e-05 - }, - { - "days": 0.979167, - "weight": 0.001203439125976838 - }, - { - "days": 0.979861, - "weight": 0.0006144678588144448 - }, - { - "days": 0.980556, - "weight": 0.0009153276403086544 - }, - { - "days": 0.98125, - "weight": 0.001225111228881082 - }, - { - "days": 0.981944, - "weight": 0.0027753040013258224 - }, - { - "days": 0.982639, - "weight": 0.0036549364133216045 - }, - { - "days": 0.983333, - "weight": 0.0008515861611785253 - }, - { - "days": 0.984722, - "weight": 0.0002919359744159914 - }, - { - "days": 0.985417, - "weight": 0.0006692855308663559 - }, - { - "days": 0.986111, - "weight": 0.006009546652388575 - }, - { - "days": 0.986806, - "weight": 0.00023074415445106748 - }, - { - "days": 0.988194, - "weight": 0.007553365276920303 - }, - { - "days": 0.988889, - "weight": 0.0007929440003788064 - }, - { - "days": 0.989583, - "weight": 0.0002995849519116069 - }, - { - "days": 0.990278, - "weight": 0.0013143492996632628 - }, - { - "days": 0.990972, - "weight": 0.00242855035485792 - }, - { - "days": 0.991667, - "weight": 2.9321080399859404e-05 - }, - { - "days": 0.995139, - "weight": 0.0008056922962048323 - }, - { - "days": 0.995833, - "weight": 0.0001032611961908092 - }, - { - "days": 0.998611, - "weight": 0.0003977468297720058 - }, - { - "days": 1.002778, - "weight": 0.0002995849519116069 - }, - { - "days": 1.007639, - "weight": 0.0010810554860469902 - }, - { - "days": 1.008333, - "weight": 3.314556914766715e-05 - }, - { - "days": 1.009722, - "weight": 0.0024451231394317535 - }, - { - "days": 1.011111, - "weight": 0.0002983101223290044 - }, - { - "days": 1.014583, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.015278, - "weight": 0.0017452416985829357 - }, - { - "days": 1.016667, - "weight": 0.0017146457886004739 - }, - { - "days": 1.017361, - "weight": 0.0013156241292458654 - }, - { - "days": 1.018056, - "weight": 0.015911148020462836 - }, - { - "days": 1.01875, - "weight": 0.001472428167905983 - }, - { - "days": 1.020139, - "weight": 0.00043981620599789105 - }, - { - "days": 1.021528, - "weight": 0.0001338571061732712 - }, - { - "days": 1.022917, - "weight": 0.0011065520776990419 - }, - { - "days": 1.023611, - "weight": 0.0001542543794949125 - }, - { - "days": 1.024306, - "weight": 0.0002970352927464018 - }, - { - "days": 1.025, - "weight": 0.00019632375572079775 - }, - { - "days": 1.027083, - "weight": 0.0005813222896667778 - }, - { - "days": 1.027778, - "weight": 0.0036115922075131168 - }, - { - "days": 1.028472, - "weight": 7.648977495615496e-05 - }, - { - "days": 1.029861, - "weight": 0.0015132227145492657 - }, - { - "days": 1.03125, - "weight": 0.00016827750490354092 - }, - { - "days": 1.031944, - "weight": 0.0017120961294352687 - }, - { - "days": 1.032639, - "weight": 0.0015565669203577534 - }, - { - "days": 1.033333, - "weight": 0.00018739994864257965 - }, - { - "days": 1.034028, - "weight": 0.0009369997432128983 - }, - { - "days": 1.034722, - "weight": 0.0041699675646930485 - }, - { - "days": 1.035417, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.036111, - "weight": 0.0009905425856822068 - }, - { - "days": 1.036806, - "weight": 0.00020269790363381066 - }, - { - "days": 1.038889, - "weight": 0.0006450637687969069 - }, - { - "days": 1.039583, - "weight": 0.0008235399103612684 - }, - { - "days": 1.040972, - "weight": 0.00042324342142405745 - }, - { - "days": 1.041667, - "weight": 0.00045511416098912205 - }, - { - "days": 1.042361, - "weight": 0.0010287874731602844 - }, - { - "days": 1.043056, - "weight": 0.0016394308432269213 - }, - { - "days": 1.04375, - "weight": 0.0009637711644475526 - }, - { - "days": 1.044444, - "weight": 0.00036460126062433867 - }, - { - "days": 1.045139, - "weight": 0.0011868663414030046 - }, - { - "days": 1.045833, - "weight": 0.0004806107526411737 - }, - { - "days": 1.046528, - "weight": 0.0005915209263275984 - }, - { - "days": 1.048611, - "weight": 0.0004640379680673401 - }, - { - "days": 1.05, - "weight": 0.0005022828555454176 - }, - { - "days": 1.050694, - "weight": 0.0003709754085373516 - }, - { - "days": 1.051389, - "weight": 0.01807453382213942 - }, - { - "days": 1.052083, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.052778, - "weight": 3.9519717060680065e-05 - }, - { - "days": 1.054861, - "weight": 0.002360984386979983 - }, - { - "days": 1.056944, - "weight": 0.00026898904192914497 - }, - { - "days": 1.058333, - "weight": 0.0004079454664328265 - }, - { - "days": 1.064583, - "weight": 0.00020269790363381066 - }, - { - "days": 1.065972, - "weight": 5.609250163451364e-05 - }, - { - "days": 1.069444, - "weight": 0.0003620516014591335 - }, - { - "days": 1.073611, - "weight": 0.00030595909982461983 - }, - { - "days": 1.074306, - "weight": 0.00010071153702560403 - }, - { - "days": 1.076389, - "weight": 0.0002728135306769527 - }, - { - "days": 1.077083, - "weight": 0.00024094279111188814 - }, - { - "days": 1.077778, - "weight": 0.00015297954991230992 - }, - { - "days": 1.079167, - "weight": 7.011562704314205e-05 - }, - { - "days": 1.079861, - "weight": 0.00013003261742546344 - }, - { - "days": 1.08125, - "weight": 8.541358203437304e-05 - }, - { - "days": 1.082639, - "weight": 0.0004806107526411737 - }, - { - "days": 1.084028, - "weight": 5.609250163451364e-05 - }, - { - "days": 1.084722, - "weight": 0.0012225615697158767 - }, - { - "days": 1.086111, - "weight": 0.000177201311981759 - }, - { - "days": 1.0875, - "weight": 5.991699038232139e-05 - }, - { - "days": 1.089583, - "weight": 0.0002817373377551708 - }, - { - "days": 1.090972, - "weight": 0.00016317818657313058 - }, - { - "days": 1.091667, - "weight": 0.0006769345083619715 - }, - { - "days": 1.092361, - "weight": 0.0010249629844124766 - }, - { - "days": 1.09375, - "weight": 0.0004283427397544678 - }, - { - "days": 1.094444, - "weight": 0.0001491550611645022 - }, - { - "days": 1.095139, - "weight": 0.00021672102904243907 - }, - { - "days": 1.097917, - "weight": 0.0011983398076464277 - }, - { - "days": 1.098611, - "weight": 8.158909328656529e-05 - }, - { - "days": 1.1, - "weight": 0.0001338571061732712 - }, - { - "days": 1.100694, - "weight": 0.0003824488747807748 - }, - { - "days": 1.101389, - "weight": 0.00036587609020694126 - }, - { - "days": 1.102083, - "weight": 0.00031233324773763274 - }, - { - "days": 1.104167, - "weight": 0.0001835754598947719 - }, - { - "days": 1.104861, - "weight": 0.00013130744700806603 - }, - { - "days": 1.105556, - "weight": 6.501630871273172e-05 - }, - { - "days": 1.10625, - "weight": 0.00018612511905997708 - }, - { - "days": 1.108333, - "weight": 0.00018867477822518224 - }, - { - "days": 1.109028, - "weight": 6.119181996492397e-05 - }, - { - "days": 1.109722, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.110417, - "weight": 0.00020269790363381066 - }, - { - "days": 1.111806, - "weight": 0.00016572784573833576 - }, - { - "days": 1.1125, - "weight": 0.0002001482444686055 - }, - { - "days": 1.113889, - "weight": 0.00012110881034724535 - }, - { - "days": 1.115972, - "weight": 6.501630871273172e-05 - }, - { - "days": 1.116667, - "weight": 0.00011600949201683502 - }, - { - "days": 1.118056, - "weight": 0.0004028461481024161 - }, - { - "days": 1.120139, - "weight": 0.00037352506770255676 - }, - { - "days": 1.120833, - "weight": 0.008078595064952567 - }, - { - "days": 1.131944, - "weight": 5.864216079971881e-05 - }, - { - "days": 1.136111, - "weight": 0.00014533057241669444 - }, - { - "days": 1.136806, - "weight": 0.00017847614156436158 - }, - { - "days": 1.1375, - "weight": 0.0001415060836688867 - }, - { - "days": 1.138889, - "weight": 0.00034930330563310766 - }, - { - "days": 1.140278, - "weight": 3.9519717060680065e-05 - }, - { - "days": 1.141667, - "weight": 7.648977495615496e-05 - }, - { - "days": 1.145833, - "weight": 0.00010453602577341178 - }, - { - "days": 1.146528, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.147222, - "weight": 8.413875245177046e-05 - }, - { - "days": 1.147917, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.148611, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.149306, - "weight": 8.668841161697562e-05 - }, - { - "days": 1.15, - "weight": 0.00010071153702560403 - }, - { - "days": 1.150694, - "weight": 0.00025624074610311915 - }, - { - "days": 1.151389, - "weight": 0.00044746518349350655 - }, - { - "days": 1.152083, - "weight": 9.178772994738595e-05 - }, - { - "days": 1.153472, - "weight": 0.00011218500326902729 - }, - { - "days": 1.154861, - "weight": 9.943670744300145e-05 - }, - { - "days": 1.155556, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.156944, - "weight": 3.69700578954749e-05 - }, - { - "days": 1.157639, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.159028, - "weight": 0.00034037949855488957 - }, - { - "days": 1.159722, - "weight": 0.00034165432813749216 - }, - { - "days": 1.160417, - "weight": 0.0010670323606383617 - }, - { - "days": 1.161111, - "weight": 0.00020397273321641325 - }, - { - "days": 1.161806, - "weight": 5.226801288670589e-05 - }, - { - "days": 1.163194, - "weight": 0.00010963534410382211 - }, - { - "days": 1.163889, - "weight": 0.0002587904052683243 - }, - { - "days": 1.165278, - "weight": 0.00023329381361627264 - }, - { - "days": 1.165972, - "weight": 3.824488747807748e-05 - }, - { - "days": 1.166667, - "weight": 3.5695228312872314e-05 - }, - { - "days": 1.167361, - "weight": 3.5695228312872314e-05 - }, - { - "days": 1.168056, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.170139, - "weight": 5.991699038232139e-05 - }, - { - "days": 1.170833, - "weight": 9.178772994738595e-05 - }, - { - "days": 1.171528, - "weight": 0.00011600949201683502 - }, - { - "days": 1.172917, - "weight": 4.716869455629556e-05 - }, - { - "days": 1.174306, - "weight": 0.00020524756279901581 - }, - { - "days": 1.176389, - "weight": 0.00016572784573833576 - }, - { - "days": 1.177083, - "weight": 0.0001415060836688867 - }, - { - "days": 1.178472, - "weight": 8.031426370396271e-05 - }, - { - "days": 1.181944, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.184722, - "weight": 4.0794546643282644e-05 - }, - { - "days": 1.1875, - "weight": 0.00013640676533847635 - }, - { - "days": 1.189583, - "weight": 2.1672102904243905e-05 - }, - { - "days": 1.191667, - "weight": 7.011562704314205e-05 - }, - { - "days": 1.193056, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.19375, - "weight": 0.00012620812867765568 - }, - { - "days": 1.194444, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.198611, - "weight": 7.011562704314205e-05 - }, - { - "days": 1.199306, - "weight": 0.0013908390746194177 - }, - { - "days": 1.200694, - "weight": 3.187073956506457e-05 - }, - { - "days": 1.202778, - "weight": 7.776460453875755e-05 - }, - { - "days": 1.203472, - "weight": 4.716869455629556e-05 - }, - { - "days": 1.204861, - "weight": 7.266528620834722e-05 - }, - { - "days": 1.205556, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.206944, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.209028, - "weight": 0.00013640676533847635 - }, - { - "days": 1.209722, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.210417, - "weight": 0.0003620516014591335 - }, - { - "days": 1.211111, - "weight": 3.824488747807748e-05 - }, - { - "days": 1.2125, - "weight": 0.0001975985853034003 - }, - { - "days": 1.213194, - "weight": 8.158909328656529e-05 - }, - { - "days": 1.213889, - "weight": 0.0001491550611645022 - }, - { - "days": 1.214583, - "weight": 0.00010581085535601436 - }, - { - "days": 1.215278, - "weight": 9.433738911259112e-05 - }, - { - "days": 1.215972, - "weight": 3.69700578954749e-05 - }, - { - "days": 1.218056, - "weight": 0.00010836051452121953 - }, - { - "days": 1.219444, - "weight": 0.00010071153702560403 - }, - { - "days": 1.220139, - "weight": 3.314556914766715e-05 - }, - { - "days": 1.220833, - "weight": 0.00061701751797965 - }, - { - "days": 1.222222, - "weight": 0.00026388972359873465 - }, - { - "days": 1.222917, - "weight": 5.226801288670589e-05 - }, - { - "days": 1.224306, - "weight": 0.00010071153702560403 - }, - { - "days": 1.225694, - "weight": 0.0007585236016485368 - }, - { - "days": 1.226389, - "weight": 8.668841161697562e-05 - }, - { - "days": 1.228472, - "weight": 0.00010581085535601436 - }, - { - "days": 1.229167, - "weight": 3.9519717060680065e-05 - }, - { - "days": 1.229861, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.231944, - "weight": 0.0001185591511820402 - }, - { - "days": 1.232639, - "weight": 7.39401157909498e-05 - }, - { - "days": 1.233333, - "weight": 9.56122186951937e-05 - }, - { - "days": 1.234028, - "weight": 0.00012238363992984793 - }, - { - "days": 1.234722, - "weight": 6.246664954752656e-05 - }, - { - "days": 1.2375, - "weight": 0.0003314556914766715 - }, - { - "days": 1.238194, - "weight": 6.501630871273172e-05 - }, - { - "days": 1.238889, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.240278, - "weight": 0.00012875778784286084 - }, - { - "days": 1.246528, - "weight": 0.001365342482967366 - }, - { - "days": 1.251389, - "weight": 5.481767205191106e-05 - }, - { - "days": 1.254167, - "weight": 0.00013513193575587378 - }, - { - "days": 1.25625, - "weight": 8.923807078218079e-05 - }, - { - "days": 1.258333, - "weight": 9.306255952998854e-05 - }, - { - "days": 1.261806, - "weight": 0.0002001482444686055 - }, - { - "days": 1.263889, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.264583, - "weight": 2.5496591652051656e-05 - }, - { - "days": 1.265972, - "weight": 8.541358203437304e-05 - }, - { - "days": 1.267361, - "weight": 0.00018994960780778484 - }, - { - "days": 1.268056, - "weight": 0.00015042989074710476 - }, - { - "days": 1.26875, - "weight": 9.816187786039887e-05 - }, - { - "days": 1.270139, - "weight": 7.011562704314205e-05 - }, - { - "days": 1.272222, - "weight": 8.031426370396271e-05 - }, - { - "days": 1.273611, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.274306, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.275, - "weight": 0.00010836051452121953 - }, - { - "days": 1.276389, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.277083, - "weight": 0.00011983398076464277 - }, - { - "days": 1.278472, - "weight": 7.39401157909498e-05 - }, - { - "days": 1.279167, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.279861, - "weight": 3.5695228312872314e-05 - }, - { - "days": 1.281944, - "weight": 3.187073956506457e-05 - }, - { - "days": 1.282639, - "weight": 0.00024604210944229846 - }, - { - "days": 1.283333, - "weight": 8.541358203437304e-05 - }, - { - "days": 1.284028, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.284722, - "weight": 0.0002970352927464018 - }, - { - "days": 1.285417, - "weight": 0.00011091017368642469 - }, - { - "days": 1.286111, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.288194, - "weight": 0.00025624074610311915 - }, - { - "days": 1.290278, - "weight": 3.5695228312872314e-05 - }, - { - "days": 1.29375, - "weight": 0.00010708568493861695 - }, - { - "days": 1.294444, - "weight": 9.178772994738595e-05 - }, - { - "days": 1.295139, - "weight": 3.314556914766715e-05 - }, - { - "days": 1.295833, - "weight": 4.0794546643282644e-05 - }, - { - "days": 1.296528, - "weight": 0.0001325822765906686 - }, - { - "days": 1.297917, - "weight": 9.178772994738595e-05 - }, - { - "days": 1.298611, - "weight": 0.0004117699551806342 - }, - { - "days": 1.299306, - "weight": 0.0001185591511820402 - }, - { - "days": 1.303472, - "weight": 0.0001988734148860029 - }, - { - "days": 1.306944, - "weight": 8.286392286916788e-05 - }, - { - "days": 1.313194, - "weight": 7.521494537355238e-05 - }, - { - "days": 1.315972, - "weight": 5.226801288670589e-05 - }, - { - "days": 1.316667, - "weight": 3.9519717060680065e-05 - }, - { - "days": 1.317361, - "weight": 3.69700578954749e-05 - }, - { - "days": 1.322917, - "weight": 7.521494537355238e-05 - }, - { - "days": 1.325, - "weight": 5.609250163451364e-05 - }, - { - "days": 1.325694, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.326389, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.327083, - "weight": 7.266528620834722e-05 - }, - { - "days": 1.327778, - "weight": 2.4221762069449073e-05 - }, - { - "days": 1.328472, - "weight": 0.00012875778784286084 - }, - { - "days": 1.329167, - "weight": 3.9519717060680065e-05 - }, - { - "days": 1.329861, - "weight": 7.266528620834722e-05 - }, - { - "days": 1.330556, - "weight": 7.903943412136013e-05 - }, - { - "days": 1.33125, - "weight": 0.0005086570034584305 - }, - { - "days": 1.332639, - "weight": 8.668841161697562e-05 - }, - { - "days": 1.333333, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.334722, - "weight": 9.56122186951937e-05 - }, - { - "days": 1.336806, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.338194, - "weight": 8.668841161697562e-05 - }, - { - "days": 1.338889, - "weight": 7.011562704314205e-05 - }, - { - "days": 1.339583, - "weight": 6.62911382953343e-05 - }, - { - "days": 1.340278, - "weight": 3.9519717060680065e-05 - }, - { - "days": 1.340972, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.342361, - "weight": 0.0002830121673377734 - }, - { - "days": 1.343056, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.344444, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.345139, - "weight": 9.178772994738595e-05 - }, - { - "days": 1.346528, - "weight": 0.00015297954991230992 - }, - { - "days": 1.348611, - "weight": 0.00016700267532093833 - }, - { - "days": 1.35, - "weight": 0.000146605401999297 - }, - { - "days": 1.352083, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.352778, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.353472, - "weight": 7.521494537355238e-05 - }, - { - "days": 1.354167, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.354861, - "weight": 4.844352413889815e-05 - }, - { - "days": 1.355556, - "weight": 7.776460453875755e-05 - }, - { - "days": 1.357639, - "weight": 0.00011600949201683502 - }, - { - "days": 1.360417, - "weight": 0.0004066706368502239 - }, - { - "days": 1.361111, - "weight": 8.286392286916788e-05 - }, - { - "days": 1.363194, - "weight": 8.796324119957821e-05 - }, - { - "days": 1.372222, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.372917, - "weight": 9.051290036478337e-05 - }, - { - "days": 1.377083, - "weight": 4.0794546643282644e-05 - }, - { - "days": 1.379861, - "weight": 4.4619035391090396e-05 - }, - { - "days": 1.382639, - "weight": 7.266528620834722e-05 - }, - { - "days": 1.385417, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.386111, - "weight": 3.059590998246198e-05 - }, - { - "days": 1.386806, - "weight": 3.5695228312872314e-05 - }, - { - "days": 1.388194, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.388889, - "weight": 3.314556914766715e-05 - }, - { - "days": 1.389583, - "weight": 6.501630871273172e-05 - }, - { - "days": 1.390972, - "weight": 5.226801288670589e-05 - }, - { - "days": 1.391667, - "weight": 8.031426370396271e-05 - }, - { - "days": 1.392361, - "weight": 9.816187786039887e-05 - }, - { - "days": 1.393056, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.395139, - "weight": 3.187073956506457e-05 - }, - { - "days": 1.396528, - "weight": 2.4221762069449073e-05 - }, - { - "days": 1.397222, - "weight": 0.00022437000653805457 - }, - { - "days": 1.397917, - "weight": 3.824488747807748e-05 - }, - { - "days": 1.398611, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.399306, - "weight": 0.00032253188439845343 - }, - { - "days": 1.4, - "weight": 6.246664954752656e-05 - }, - { - "days": 1.402083, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.402778, - "weight": 2.1672102904243905e-05 - }, - { - "days": 1.403472, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.404167, - "weight": 0.0002358434727814778 - }, - { - "days": 1.405556, - "weight": 3.059590998246198e-05 - }, - { - "days": 1.407639, - "weight": 8.541358203437304e-05 - }, - { - "days": 1.409028, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.409722, - "weight": 2.4221762069449073e-05 - }, - { - "days": 1.410417, - "weight": 3.824488747807748e-05 - }, - { - "days": 1.4125, - "weight": 0.00011600949201683502 - }, - { - "days": 1.413194, - "weight": 8.796324119957821e-05 - }, - { - "days": 1.414583, - "weight": 0.00017337682323395124 - }, - { - "days": 1.415972, - "weight": 2.5496591652051656e-05 - }, - { - "days": 1.418056, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.41875, - "weight": 4.206937622588523e-05 - }, - { - "days": 1.419444, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.420139, - "weight": 0.0002677142123465424 - }, - { - "days": 1.426389, - "weight": 8.413875245177046e-05 - }, - { - "days": 1.430556, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.435417, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.4375, - "weight": 3.059590998246198e-05 - }, - { - "days": 1.440278, - "weight": 3.314556914766715e-05 - }, - { - "days": 1.441667, - "weight": 3.187073956506457e-05 - }, - { - "days": 1.445833, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.447222, - "weight": 3.314556914766715e-05 - }, - { - "days": 1.448611, - "weight": 0.00022182034737284939 - }, - { - "days": 1.449306, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.45, - "weight": 3.9519717060680065e-05 - }, - { - "days": 1.451389, - "weight": 0.00011091017368642469 - }, - { - "days": 1.453472, - "weight": 5.609250163451364e-05 - }, - { - "days": 1.454167, - "weight": 0.0001542543794949125 - }, - { - "days": 1.45625, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.456944, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.458333, - "weight": 0.00010198636660820662 - }, - { - "days": 1.460417, - "weight": 0.0002294693248684649 - }, - { - "days": 1.461111, - "weight": 0.00020524756279901581 - }, - { - "days": 1.463889, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.464583, - "weight": 6.119181996492397e-05 - }, - { - "days": 1.465972, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.468056, - "weight": 4.844352413889815e-05 - }, - { - "days": 1.469444, - "weight": 4.844352413889815e-05 - }, - { - "days": 1.470139, - "weight": 0.00016572784573833576 - }, - { - "days": 1.471528, - "weight": 0.00010581085535601436 - }, - { - "days": 1.472222, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.472917, - "weight": 3.5695228312872314e-05 - }, - { - "days": 1.474306, - "weight": 4.206937622588523e-05 - }, - { - "days": 1.475694, - "weight": 5.864216079971881e-05 - }, - { - "days": 1.476389, - "weight": 3.059590998246198e-05 - }, - { - "days": 1.477778, - "weight": 7.266528620834722e-05 - }, - { - "days": 1.479167, - "weight": 3.187073956506457e-05 - }, - { - "days": 1.48125, - "weight": 0.0003008597814942095 - }, - { - "days": 1.482639, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.495833, - "weight": 5.226801288670589e-05 - }, - { - "days": 1.498611, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.499306, - "weight": 0.0001491550611645022 - }, - { - "days": 1.504167, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.507639, - "weight": 6.62911382953343e-05 - }, - { - "days": 1.508333, - "weight": 6.501630871273172e-05 - }, - { - "days": 1.509028, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.509722, - "weight": 7.521494537355238e-05 - }, - { - "days": 1.510417, - "weight": 2.4221762069449073e-05 - }, - { - "days": 1.513194, - "weight": 8.413875245177046e-05 - }, - { - "days": 1.513889, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.515278, - "weight": 2.5496591652051656e-05 - }, - { - "days": 1.518056, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.51875, - "weight": 3.187073956506457e-05 - }, - { - "days": 1.519444, - "weight": 8.541358203437304e-05 - }, - { - "days": 1.520139, - "weight": 0.00034547881688529994 - }, - { - "days": 1.521528, - "weight": 7.903943412136013e-05 - }, - { - "days": 1.522222, - "weight": 0.00012238363992984793 - }, - { - "days": 1.523611, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.524306, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.527083, - "weight": 9.56122186951937e-05 - }, - { - "days": 1.527778, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.528472, - "weight": 5.481767205191106e-05 - }, - { - "days": 1.529167, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.530556, - "weight": 3.314556914766715e-05 - }, - { - "days": 1.53125, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.532639, - "weight": 9.433738911259112e-05 - }, - { - "days": 1.534028, - "weight": 5.226801288670589e-05 - }, - { - "days": 1.534722, - "weight": 6.119181996492397e-05 - }, - { - "days": 1.535417, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.536111, - "weight": 5.481767205191106e-05 - }, - { - "days": 1.536806, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.538194, - "weight": 0.00032125705481585084 - }, - { - "days": 1.545833, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.550694, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.55625, - "weight": 3.187073956506457e-05 - }, - { - "days": 1.558333, - "weight": 0.0003021346110768121 - }, - { - "days": 1.564583, - "weight": 2.677142123465424e-05 - }, - { - "days": 1.565972, - "weight": 3.5695228312872314e-05 - }, - { - "days": 1.567361, - "weight": 3.9519717060680065e-05 - }, - { - "days": 1.568056, - "weight": 3.187073956506457e-05 - }, - { - "days": 1.570139, - "weight": 3.187073956506457e-05 - }, - { - "days": 1.572917, - "weight": 0.00010453602577341178 - }, - { - "days": 1.574306, - "weight": 5.609250163451364e-05 - }, - { - "days": 1.577778, - "weight": 3.314556914766715e-05 - }, - { - "days": 1.578472, - "weight": 3.69700578954749e-05 - }, - { - "days": 1.579167, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.580556, - "weight": 0.00010836051452121953 - }, - { - "days": 1.581944, - "weight": 0.000354402623963518 - }, - { - "days": 1.582639, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.583333, - "weight": 5.609250163451364e-05 - }, - { - "days": 1.585417, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.586806, - "weight": 2.5496591652051656e-05 - }, - { - "days": 1.5875, - "weight": 4.4619035391090396e-05 - }, - { - "days": 1.588194, - "weight": 0.00011218500326902729 - }, - { - "days": 1.590972, - "weight": 7.39401157909498e-05 - }, - { - "days": 1.592361, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.593056, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.594444, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.595833, - "weight": 0.0002893863152507863 - }, - { - "days": 1.596528, - "weight": 5.481767205191106e-05 - }, - { - "days": 1.597917, - "weight": 5.609250163451364e-05 - }, - { - "days": 1.599306, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.6, - "weight": 2.4221762069449073e-05 - }, - { - "days": 1.601389, - "weight": 2.1672102904243905e-05 - }, - { - "days": 1.615278, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.621528, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.625, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.627083, - "weight": 2.677142123465424e-05 - }, - { - "days": 1.634722, - "weight": 7.39401157909498e-05 - }, - { - "days": 1.6375, - "weight": 2.5496591652051656e-05 - }, - { - "days": 1.639583, - "weight": 5.226801288670589e-05 - }, - { - "days": 1.640972, - "weight": 5.481767205191106e-05 - }, - { - "days": 1.641667, - "weight": 2.4221762069449073e-05 - }, - { - "days": 1.642361, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.64375, - "weight": 0.00011983398076464277 - }, - { - "days": 1.644444, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.645833, - "weight": 4.844352413889815e-05 - }, - { - "days": 1.646528, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.647917, - "weight": 0.00015680403866011767 - }, - { - "days": 1.650694, - "weight": 4.4619035391090396e-05 - }, - { - "days": 1.653472, - "weight": 7.139045662574463e-05 - }, - { - "days": 1.654861, - "weight": 0.00015680403866011767 - }, - { - "days": 1.655556, - "weight": 0.00011728432159943762 - }, - { - "days": 1.65625, - "weight": 4.206937622588523e-05 - }, - { - "days": 1.656944, - "weight": 8.541358203437304e-05 - }, - { - "days": 1.658333, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.659722, - "weight": 0.00033655500980708184 - }, - { - "days": 1.6625, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.668056, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.670833, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.671528, - "weight": 9.816187786039887e-05 - }, - { - "days": 1.676389, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.684028, - "weight": 2.4221762069449073e-05 - }, - { - "days": 1.689583, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.691667, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.69375, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.696528, - "weight": 3.059590998246198e-05 - }, - { - "days": 1.697917, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.698611, - "weight": 2.677142123465424e-05 - }, - { - "days": 1.699306, - "weight": 6.62911382953343e-05 - }, - { - "days": 1.700694, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.703472, - "weight": 7.521494537355238e-05 - }, - { - "days": 1.704861, - "weight": 3.059590998246198e-05 - }, - { - "days": 1.705556, - "weight": 4.4619035391090396e-05 - }, - { - "days": 1.709722, - "weight": 0.0001389564245036815 - }, - { - "days": 1.711806, - "weight": 8.541358203437304e-05 - }, - { - "days": 1.7125, - "weight": 3.824488747807748e-05 - }, - { - "days": 1.713889, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.714583, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.715278, - "weight": 2.4221762069449073e-05 - }, - { - "days": 1.715972, - "weight": 7.266528620834722e-05 - }, - { - "days": 1.716667, - "weight": 0.0001338571061732712 - }, - { - "days": 1.71875, - "weight": 3.059590998246198e-05 - }, - { - "days": 1.720139, - "weight": 0.0005545508684321235 - }, - { - "days": 1.721528, - "weight": 4.5893864973692975e-05 - }, - { - "days": 1.731944, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.735417, - "weight": 8.158909328656529e-05 - }, - { - "days": 1.738889, - "weight": 5.226801288670589e-05 - }, - { - "days": 1.743056, - "weight": 2.677142123465424e-05 - }, - { - "days": 1.744444, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.748611, - "weight": 3.5695228312872314e-05 - }, - { - "days": 1.757639, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.758333, - "weight": 5.099318330410331e-05 - }, - { - "days": 1.760417, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.761111, - "weight": 6.119181996492397e-05 - }, - { - "days": 1.7625, - "weight": 0.00015170472032970735 - }, - { - "days": 1.763194, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.766667, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.772222, - "weight": 6.119181996492397e-05 - }, - { - "days": 1.773611, - "weight": 0.00010198636660820662 - }, - { - "days": 1.775, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.775694, - "weight": 7.139045662574463e-05 - }, - { - "days": 1.777083, - "weight": 6.756596787793689e-05 - }, - { - "days": 1.777778, - "weight": 7.011562704314205e-05 - }, - { - "days": 1.779167, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.781944, - "weight": 0.0001402312540862841 - }, - { - "days": 1.782639, - "weight": 0.0004755114343107634 - }, - { - "days": 1.789583, - "weight": 8.923807078218079e-05 - }, - { - "days": 1.810417, - "weight": 4.206937622588523e-05 - }, - { - "days": 1.813194, - "weight": 2.677142123465424e-05 - }, - { - "days": 1.815278, - "weight": 2.804625081725682e-05 - }, - { - "days": 1.819444, - "weight": 7.776460453875755e-05 - }, - { - "days": 1.820833, - "weight": 2.677142123465424e-05 - }, - { - "days": 1.821528, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.823611, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.825, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.825694, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.827083, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.828472, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.829167, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.83125, - "weight": 4.0794546643282644e-05 - }, - { - "days": 1.831944, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.834028, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.834722, - "weight": 7.903943412136013e-05 - }, - { - "days": 1.835417, - "weight": 3.059590998246198e-05 - }, - { - "days": 1.836806, - "weight": 0.00014278091325148925 - }, - { - "days": 1.8375, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.838889, - "weight": 6.119181996492397e-05 - }, - { - "days": 1.839583, - "weight": 3.69700578954749e-05 - }, - { - "days": 1.840278, - "weight": 6.501630871273172e-05 - }, - { - "days": 1.840972, - "weight": 0.00011091017368642469 - }, - { - "days": 1.844444, - "weight": 0.0004933590484671995 - }, - { - "days": 1.845833, - "weight": 3.314556914766715e-05 - }, - { - "days": 1.847917, - "weight": 5.864216079971881e-05 - }, - { - "days": 1.849306, - "weight": 4.0794546643282644e-05 - }, - { - "days": 1.85625, - "weight": 6.884079746053947e-05 - }, - { - "days": 1.857639, - "weight": 2.677142123465424e-05 - }, - { - "days": 1.863889, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.867361, - "weight": 2.677142123465424e-05 - }, - { - "days": 1.868056, - "weight": 4.0794546643282644e-05 - }, - { - "days": 1.872222, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.872917, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.878472, - "weight": 8.158909328656529e-05 - }, - { - "days": 1.880556, - "weight": 3.4420398730269734e-05 - }, - { - "days": 1.88125, - "weight": 3.824488747807748e-05 - }, - { - "days": 1.882639, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.883333, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.884722, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.885417, - "weight": 2.5496591652051656e-05 - }, - { - "days": 1.886806, - "weight": 2.5496591652051656e-05 - }, - { - "days": 1.888194, - "weight": 4.716869455629556e-05 - }, - { - "days": 1.890278, - "weight": 0.00019632375572079775 - }, - { - "days": 1.891667, - "weight": 7.903943412136013e-05 - }, - { - "days": 1.894444, - "weight": 4.0794546643282644e-05 - }, - { - "days": 1.895833, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.897222, - "weight": 5.481767205191106e-05 - }, - { - "days": 1.897917, - "weight": 0.00019632375572079775 - }, - { - "days": 1.898611, - "weight": 6.374147913012914e-05 - }, - { - "days": 1.899306, - "weight": 0.00013513193575587378 - }, - { - "days": 1.9, - "weight": 3.5695228312872314e-05 - }, - { - "days": 1.900694, - "weight": 9.68870482777963e-05 - }, - { - "days": 1.906944, - "weight": 7.776460453875755e-05 - }, - { - "days": 1.907639, - "weight": 0.0007648977495615496 - }, - { - "days": 1.908333, - "weight": 3.9519717060680065e-05 - }, - { - "days": 1.909722, - "weight": 4.0794546643282644e-05 - }, - { - "days": 1.916667, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.921528, - "weight": 2.1672102904243905e-05 - }, - { - "days": 1.922917, - "weight": 5.481767205191106e-05 - }, - { - "days": 1.923611, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.925694, - "weight": 2.4221762069449073e-05 - }, - { - "days": 1.932639, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.933333, - "weight": 9.433738911259112e-05 - }, - { - "days": 1.936111, - "weight": 3.187073956506457e-05 - }, - { - "days": 1.939583, - "weight": 4.0794546643282644e-05 - }, - { - "days": 1.940278, - "weight": 8.923807078218079e-05 - }, - { - "days": 1.941667, - "weight": 2.677142123465424e-05 - }, - { - "days": 1.942361, - "weight": 7.521494537355238e-05 - }, - { - "days": 1.943056, - "weight": 4.206937622588523e-05 - }, - { - "days": 1.945139, - "weight": 4.206937622588523e-05 - }, - { - "days": 1.945833, - "weight": 7.903943412136013e-05 - }, - { - "days": 1.946528, - "weight": 0.00012493329909505312 - }, - { - "days": 1.947222, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.947917, - "weight": 3.824488747807748e-05 - }, - { - "days": 1.949306, - "weight": 5.354284246930848e-05 - }, - { - "days": 1.95, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.950694, - "weight": 7.139045662574463e-05 - }, - { - "days": 1.951389, - "weight": 7.776460453875755e-05 - }, - { - "days": 1.954861, - "weight": 0.00016445301615573317 - }, - { - "days": 1.955556, - "weight": 2.9321080399859404e-05 - }, - { - "days": 1.956944, - "weight": 0.00011218500326902729 - }, - { - "days": 1.958333, - "weight": 4.844352413889815e-05 - }, - { - "days": 1.959028, - "weight": 0.000323806713981056 - }, - { - "days": 1.959722, - "weight": 0.00013130744700806603 - }, - { - "days": 1.961111, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.963194, - "weight": 2.2946932486846487e-05 - }, - { - "days": 1.963889, - "weight": 3.5695228312872314e-05 - }, - { - "days": 1.964583, - "weight": 0.000161903356990528 - }, - { - "days": 1.965972, - "weight": 0.00031998222523324824 - }, - { - "days": 1.966667, - "weight": 3.824488747807748e-05 - }, - { - "days": 1.968056, - "weight": 3.69700578954749e-05 - }, - { - "days": 1.970139, - "weight": 4.334420580848781e-05 - }, - { - "days": 1.970833, - "weight": 3.69700578954749e-05 - }, - { - "days": 1.972917, - "weight": 7.39401157909498e-05 - }, - { - "days": 1.975694, - "weight": 3.059590998246198e-05 - }, - { - "days": 1.98125, - "weight": 4.4619035391090396e-05 - }, - { - "days": 1.981944, - "weight": 5.736733121711622e-05 - }, - { - "days": 1.982639, - "weight": 0.0001415060836688867 - }, - { - "days": 1.9875, - "weight": 7.39401157909498e-05 - }, - { - "days": 1.988194, - "weight": 7.776460453875755e-05 - }, - { - "days": 1.990972, - "weight": 4.716869455629556e-05 - }, - { - "days": 1.99375, - "weight": 8.413875245177046e-05 - }, - { - "days": 1.997222, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.997917, - "weight": 4.9718353721500726e-05 - }, - { - "days": 1.998611, - "weight": 0.00019632375572079775 - }, - { - "days": 1.999306, - "weight": 6.62911382953343e-05 - }, - { - "days": 2.001389, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.002083, - "weight": 9.051290036478337e-05 - }, - { - "days": 2.004167, - "weight": 0.0002983101223290044 - }, - { - "days": 2.004861, - "weight": 0.00015297954991230992 - }, - { - "days": 2.005556, - "weight": 2.677142123465424e-05 - }, - { - "days": 2.006944, - "weight": 2.804625081725682e-05 - }, - { - "days": 2.008333, - "weight": 6.374147913012914e-05 - }, - { - "days": 2.009028, - "weight": 0.00015042989074710476 - }, - { - "days": 2.009722, - "weight": 0.00013003261742546344 - }, - { - "days": 2.010417, - "weight": 0.00010708568493861695 - }, - { - "days": 2.013194, - "weight": 3.187073956506457e-05 - }, - { - "days": 2.014583, - "weight": 5.354284246930848e-05 - }, - { - "days": 2.015278, - "weight": 4.206937622588523e-05 - }, - { - "days": 2.015972, - "weight": 0.00020397273321641325 - }, - { - "days": 2.018056, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.01875, - "weight": 7.648977495615496e-05 - }, - { - "days": 2.020139, - "weight": 9.433738911259112e-05 - }, - { - "days": 2.020833, - "weight": 0.0002664393827639398 - }, - { - "days": 2.023611, - "weight": 9.433738911259112e-05 - }, - { - "days": 2.024306, - "weight": 4.716869455629556e-05 - }, - { - "days": 2.025694, - "weight": 0.0003990216593546084 - }, - { - "days": 2.03125, - "weight": 0.00023839313194668296 - }, - { - "days": 2.031944, - "weight": 4.4619035391090396e-05 - }, - { - "days": 2.034028, - "weight": 7.266528620834722e-05 - }, - { - "days": 2.041667, - "weight": 4.9718353721500726e-05 - }, - { - "days": 2.042361, - "weight": 7.266528620834722e-05 - }, - { - "days": 2.044444, - "weight": 3.824488747807748e-05 - }, - { - "days": 2.045139, - "weight": 3.9519717060680065e-05 - }, - { - "days": 2.046528, - "weight": 3.5695228312872314e-05 - }, - { - "days": 2.048611, - "weight": 3.69700578954749e-05 - }, - { - "days": 2.049306, - "weight": 5.736733121711622e-05 - }, - { - "days": 2.052778, - "weight": 6.756596787793689e-05 - }, - { - "days": 2.053472, - "weight": 3.824488747807748e-05 - }, - { - "days": 2.054167, - "weight": 2.804625081725682e-05 - }, - { - "days": 2.056944, - "weight": 7.776460453875755e-05 - }, - { - "days": 2.057639, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.059028, - "weight": 2.804625081725682e-05 - }, - { - "days": 2.059722, - "weight": 5.736733121711622e-05 - }, - { - "days": 2.063889, - "weight": 9.051290036478337e-05 - }, - { - "days": 2.065278, - "weight": 4.716869455629556e-05 - }, - { - "days": 2.065972, - "weight": 2.804625081725682e-05 - }, - { - "days": 2.066667, - "weight": 5.481767205191106e-05 - }, - { - "days": 2.068056, - "weight": 8.413875245177046e-05 - }, - { - "days": 2.070833, - "weight": 0.00012875778784286084 - }, - { - "days": 2.071528, - "weight": 4.9718353721500726e-05 - }, - { - "days": 2.072917, - "weight": 6.119181996492397e-05 - }, - { - "days": 2.073611, - "weight": 3.824488747807748e-05 - }, - { - "days": 2.075, - "weight": 0.00012875778784286084 - }, - { - "days": 2.077083, - "weight": 9.178772994738595e-05 - }, - { - "days": 2.078472, - "weight": 7.776460453875755e-05 - }, - { - "days": 2.079861, - "weight": 6.62911382953343e-05 - }, - { - "days": 2.080556, - "weight": 3.4420398730269734e-05 - }, - { - "days": 2.082639, - "weight": 5.481767205191106e-05 - }, - { - "days": 2.083333, - "weight": 2.1672102904243905e-05 - }, - { - "days": 2.086806, - "weight": 0.0002804625081725682 - }, - { - "days": 2.093056, - "weight": 0.0003951971706068006 - }, - { - "days": 2.095139, - "weight": 2.677142123465424e-05 - }, - { - "days": 2.108333, - "weight": 6.119181996492397e-05 - }, - { - "days": 2.110417, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.115972, - "weight": 3.5695228312872314e-05 - }, - { - "days": 2.117361, - "weight": 2.1672102904243905e-05 - }, - { - "days": 2.11875, - "weight": 0.00013513193575587378 - }, - { - "days": 2.120833, - "weight": 4.4619035391090396e-05 - }, - { - "days": 2.122917, - "weight": 2.2946932486846487e-05 - }, - { - "days": 2.125, - "weight": 3.4420398730269734e-05 - }, - { - "days": 2.126389, - "weight": 2.677142123465424e-05 - }, - { - "days": 2.127778, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.129861, - "weight": 3.5695228312872314e-05 - }, - { - "days": 2.130556, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.132639, - "weight": 2.2946932486846487e-05 - }, - { - "days": 2.134028, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.136111, - "weight": 8.158909328656529e-05 - }, - { - "days": 2.1375, - "weight": 0.00011728432159943762 - }, - { - "days": 2.138194, - "weight": 6.62911382953343e-05 - }, - { - "days": 2.138889, - "weight": 3.314556914766715e-05 - }, - { - "days": 2.140278, - "weight": 0.00012110881034724535 - }, - { - "days": 2.140972, - "weight": 3.69700578954749e-05 - }, - { - "days": 2.145139, - "weight": 3.314556914766715e-05 - }, - { - "days": 2.15, - "weight": 9.56122186951937e-05 - }, - { - "days": 2.152778, - "weight": 2.2946932486846487e-05 - }, - { - "days": 2.154861, - "weight": 0.00014533057241669444 - }, - { - "days": 2.159028, - "weight": 7.903943412136013e-05 - }, - { - "days": 2.164583, - "weight": 2.804625081725682e-05 - }, - { - "days": 2.165972, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.168056, - "weight": 2.4221762069449073e-05 - }, - { - "days": 2.178472, - "weight": 2.1672102904243905e-05 - }, - { - "days": 2.180556, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.181944, - "weight": 8.413875245177046e-05 - }, - { - "days": 2.182639, - "weight": 4.206937622588523e-05 - }, - { - "days": 2.184722, - "weight": 8.541358203437304e-05 - }, - { - "days": 2.188194, - "weight": 2.677142123465424e-05 - }, - { - "days": 2.189583, - "weight": 4.716869455629556e-05 - }, - { - "days": 2.190972, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.191667, - "weight": 2.804625081725682e-05 - }, - { - "days": 2.195139, - "weight": 4.844352413889815e-05 - }, - { - "days": 2.196528, - "weight": 2.2946932486846487e-05 - }, - { - "days": 2.197222, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.197917, - "weight": 2.804625081725682e-05 - }, - { - "days": 2.198611, - "weight": 5.226801288670589e-05 - }, - { - "days": 2.199306, - "weight": 4.334420580848781e-05 - }, - { - "days": 2.2, - "weight": 4.9718353721500726e-05 - }, - { - "days": 2.204167, - "weight": 3.059590998246198e-05 - }, - { - "days": 2.206944, - "weight": 0.00010836051452121953 - }, - { - "days": 2.210417, - "weight": 0.00020269790363381066 - }, - { - "days": 2.211806, - "weight": 2.1672102904243905e-05 - }, - { - "days": 2.220833, - "weight": 0.0001032611961908092 - }, - { - "days": 2.238889, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.241667, - "weight": 3.69700578954749e-05 - }, - { - "days": 2.24375, - "weight": 5.226801288670589e-05 - }, - { - "days": 2.245139, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.246528, - "weight": 3.9519717060680065e-05 - }, - { - "days": 2.247917, - "weight": 7.139045662574463e-05 - }, - { - "days": 2.250694, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.252778, - "weight": 4.0794546643282644e-05 - }, - { - "days": 2.253472, - "weight": 3.059590998246198e-05 - }, - { - "days": 2.254167, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.25625, - "weight": 6.119181996492397e-05 - }, - { - "days": 2.256944, - "weight": 9.943670744300145e-05 - }, - { - "days": 2.258333, - "weight": 3.059590998246198e-05 - }, - { - "days": 2.259028, - "weight": 3.9519717060680065e-05 - }, - { - "days": 2.261806, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.266667, - "weight": 7.521494537355238e-05 - }, - { - "days": 2.269444, - "weight": 0.00016827750490354092 - }, - { - "days": 2.272222, - "weight": 0.0001402312540862841 - }, - { - "days": 2.299306, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.302778, - "weight": 4.334420580848781e-05 - }, - { - "days": 2.309722, - "weight": 3.314556914766715e-05 - }, - { - "days": 2.311806, - "weight": 5.354284246930848e-05 - }, - { - "days": 2.3125, - "weight": 4.4619035391090396e-05 - }, - { - "days": 2.313194, - "weight": 2.4221762069449073e-05 - }, - { - "days": 2.314583, - "weight": 4.716869455629556e-05 - }, - { - "days": 2.315972, - "weight": 4.716869455629556e-05 - }, - { - "days": 2.318056, - "weight": 9.56122186951937e-05 - }, - { - "days": 2.31875, - "weight": 4.0794546643282644e-05 - }, - { - "days": 2.325694, - "weight": 7.266528620834722e-05 - }, - { - "days": 2.330556, - "weight": 0.00018994960780778484 - }, - { - "days": 2.33125, - "weight": 9.433738911259112e-05 - }, - { - "days": 2.336111, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.338194, - "weight": 9.178772994738595e-05 - }, - { - "days": 2.360417, - "weight": 4.0794546643282644e-05 - }, - { - "days": 2.36875, - "weight": 3.69700578954749e-05 - }, - { - "days": 2.373611, - "weight": 7.903943412136013e-05 - }, - { - "days": 2.375, - "weight": 2.4221762069449073e-05 - }, - { - "days": 2.376389, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.379167, - "weight": 5.864216079971881e-05 - }, - { - "days": 2.379861, - "weight": 2.4221762069449073e-05 - }, - { - "days": 2.384722, - "weight": 0.00012875778784286084 - }, - { - "days": 2.385417, - "weight": 3.69700578954749e-05 - }, - { - "days": 2.386111, - "weight": 0.00020142307405120806 - }, - { - "days": 2.392361, - "weight": 0.00010836051452121953 - }, - { - "days": 2.39375, - "weight": 2.2946932486846487e-05 - }, - { - "days": 2.397222, - "weight": 5.991699038232139e-05 - }, - { - "days": 2.421528, - "weight": 3.5695228312872314e-05 - }, - { - "days": 2.43125, - "weight": 5.991699038232139e-05 - }, - { - "days": 2.432639, - "weight": 4.716869455629556e-05 - }, - { - "days": 2.434722, - "weight": 6.119181996492397e-05 - }, - { - "days": 2.435417, - "weight": 2.1672102904243905e-05 - }, - { - "days": 2.436806, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.4375, - "weight": 3.059590998246198e-05 - }, - { - "days": 2.440278, - "weight": 0.0001338571061732712 - }, - { - "days": 2.44375, - "weight": 3.059590998246198e-05 - }, - { - "days": 2.447917, - "weight": 9.306255952998854e-05 - }, - { - "days": 2.448611, - "weight": 3.059590998246198e-05 - }, - { - "days": 2.450694, - "weight": 0.00020779722196422097 - }, - { - "days": 2.451389, - "weight": 4.334420580848781e-05 - }, - { - "days": 2.458333, - "weight": 0.00010581085535601436 - }, - { - "days": 2.478472, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.49375, - "weight": 3.059590998246198e-05 - }, - { - "days": 2.495139, - "weight": 2.2946932486846487e-05 - }, - { - "days": 2.495833, - "weight": 5.609250163451364e-05 - }, - { - "days": 2.496528, - "weight": 4.844352413889815e-05 - }, - { - "days": 2.499306, - "weight": 0.00020779722196422097 - }, - { - "days": 2.501389, - "weight": 3.187073956506457e-05 - }, - { - "days": 2.505556, - "weight": 0.0002791876785899656 - }, - { - "days": 2.509722, - "weight": 4.9718353721500726e-05 - }, - { - "days": 2.511111, - "weight": 3.824488747807748e-05 - }, - { - "days": 2.5125, - "weight": 0.00010708568493861695 - }, - { - "days": 2.513194, - "weight": 3.4420398730269734e-05 - }, - { - "days": 2.513889, - "weight": 8.158909328656529e-05 - }, - { - "days": 2.532639, - "weight": 2.804625081725682e-05 - }, - { - "days": 2.538194, - "weight": 3.059590998246198e-05 - }, - { - "days": 2.552083, - "weight": 2.2946932486846487e-05 - }, - { - "days": 2.554167, - "weight": 3.9519717060680065e-05 - }, - { - "days": 2.557639, - "weight": 3.059590998246198e-05 - }, - { - "days": 2.561111, - "weight": 2.4221762069449073e-05 - }, - { - "days": 2.5625, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.565278, - "weight": 5.481767205191106e-05 - }, - { - "days": 2.565972, - "weight": 0.00035695228312872316 - }, - { - "days": 2.567361, - "weight": 0.00016062852740792542 - }, - { - "days": 2.568056, - "weight": 4.206937622588523e-05 - }, - { - "days": 2.570139, - "weight": 6.246664954752656e-05 - }, - { - "days": 2.570833, - "weight": 0.00015170472032970735 - }, - { - "days": 2.578472, - "weight": 0.00011345983285162986 - }, - { - "days": 2.602083, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.620139, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.621528, - "weight": 0.0001389564245036815 - }, - { - "days": 2.628472, - "weight": 0.0001415060836688867 - }, - { - "days": 2.629167, - "weight": 7.139045662574463e-05 - }, - { - "days": 2.629861, - "weight": 9.306255952998854e-05 - }, - { - "days": 2.63125, - "weight": 4.844352413889815e-05 - }, - { - "days": 2.632639, - "weight": 0.0001402312540862841 - }, - { - "days": 2.633333, - "weight": 0.00012365846951245053 - }, - { - "days": 2.636806, - "weight": 0.00043726654683268586 - }, - { - "days": 2.672917, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.674306, - "weight": 4.5893864973692975e-05 - }, - { - "days": 2.680556, - "weight": 0.0002141713698772339 - }, - { - "days": 2.684722, - "weight": 8.668841161697562e-05 - }, - { - "days": 2.686806, - "weight": 6.374147913012914e-05 - }, - { - "days": 2.688194, - "weight": 0.0003786243860329671 - }, - { - "days": 2.689583, - "weight": 4.4619035391090396e-05 - }, - { - "days": 2.690278, - "weight": 0.0001032611961908092 - }, - { - "days": 2.69375, - "weight": 2.804625081725682e-05 - }, - { - "days": 2.694444, - "weight": 7.139045662574463e-05 - }, - { - "days": 2.698611, - "weight": 0.00010071153702560403 - }, - { - "days": 2.700694, - "weight": 3.187073956506457e-05 - }, - { - "days": 2.713889, - "weight": 2.1672102904243905e-05 - }, - { - "days": 2.729861, - "weight": 4.334420580848781e-05 - }, - { - "days": 2.731944, - "weight": 2.1672102904243905e-05 - }, - { - "days": 2.739583, - "weight": 0.00015807886824272026 - }, - { - "days": 2.747222, - "weight": 0.0005532760388495209 - }, - { - "days": 2.748611, - "weight": 5.609250163451364e-05 - }, - { - "days": 2.75, - "weight": 6.374147913012914e-05 - }, - { - "days": 2.750694, - "weight": 4.0794546643282644e-05 - }, - { - "days": 2.751389, - "weight": 4.206937622588523e-05 - }, - { - "days": 2.754861, - "weight": 8.541358203437304e-05 - }, - { - "days": 2.75625, - "weight": 0.0001389564245036815 - }, - { - "days": 2.757639, - "weight": 2.2946932486846487e-05 - }, - { - "days": 2.780556, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.793056, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.794444, - "weight": 3.9519717060680065e-05 - }, - { - "days": 2.804861, - "weight": 5.226801288670589e-05 - }, - { - "days": 2.805556, - "weight": 2.4221762069449073e-05 - }, - { - "days": 2.80625, - "weight": 0.001004565711090835 - }, - { - "days": 2.807639, - "weight": 6.756596787793689e-05 - }, - { - "days": 2.809028, - "weight": 2.2946932486846487e-05 - }, - { - "days": 2.813889, - "weight": 0.0002893863152507863 - }, - { - "days": 2.816667, - "weight": 6.756596787793689e-05 - }, - { - "days": 2.822222, - "weight": 3.5695228312872314e-05 - }, - { - "days": 2.848611, - "weight": 4.4619035391090396e-05 - }, - { - "days": 2.849306, - "weight": 4.4619035391090396e-05 - }, - { - "days": 2.859028, - "weight": 2.4221762069449073e-05 - }, - { - "days": 2.864583, - "weight": 3.5695228312872314e-05 - }, - { - "days": 2.867361, - "weight": 5.864216079971881e-05 - }, - { - "days": 2.868056, - "weight": 3.69700578954749e-05 - }, - { - "days": 2.870833, - "weight": 5.991699038232139e-05 - }, - { - "days": 2.871528, - "weight": 0.00013640676533847635 - }, - { - "days": 2.873611, - "weight": 3.5695228312872314e-05 - }, - { - "days": 2.875, - "weight": 2.804625081725682e-05 - }, - { - "days": 2.875694, - "weight": 0.00023074415445106748 - }, - { - "days": 2.880556, - "weight": 0.004813756503907352 - }, - { - "days": 2.882639, - "weight": 0.00011218500326902729 - }, - { - "days": 2.890972, - "weight": 5.226801288670589e-05 - }, - { - "days": 2.895833, - "weight": 4.4619035391090396e-05 - }, - { - "days": 2.913194, - "weight": 8.031426370396271e-05 - }, - { - "days": 2.914583, - "weight": 0.00012365846951245053 - }, - { - "days": 2.91875, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.919444, - "weight": 3.9519717060680065e-05 - }, - { - "days": 2.922917, - "weight": 0.0001325822765906686 - }, - { - "days": 2.925694, - "weight": 0.0002358434727814778 - }, - { - "days": 2.929167, - "weight": 0.00020269790363381066 - }, - { - "days": 2.934028, - "weight": 0.00018739994864257965 - }, - { - "days": 2.9375, - "weight": 5.099318330410331e-05 - }, - { - "days": 2.940972, - "weight": 0.00010581085535601436 - }, - { - "days": 2.941667, - "weight": 0.0006833086562749843 - }, - { - "days": 2.942361, - "weight": 0.003034094406594147 - }, - { - "days": 2.95, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.954167, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.957639, - "weight": 6.246664954752656e-05 - }, - { - "days": 2.959028, - "weight": 3.314556914766715e-05 - }, - { - "days": 2.959722, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.960417, - "weight": 2.9321080399859404e-05 - }, - { - "days": 2.965278, - "weight": 2.4221762069449073e-05 - }, - { - "days": 2.965972, - "weight": 3.314556914766715e-05 - }, - { - "days": 2.968056, - "weight": 0.0002715387010943501 - }, - { - "days": 2.969444, - "weight": 3.824488747807748e-05 - }, - { - "days": 2.971528, - "weight": 2.677142123465424e-05 - }, - { - "days": 2.972222, - "weight": 0.00032635637314626115 - }, - { - "days": 2.972917, - "weight": 3.314556914766715e-05 - }, - { - "days": 2.974306, - "weight": 8.541358203437304e-05 - }, - { - "days": 2.976389, - "weight": 2.5496591652051656e-05 - }, - { - "days": 2.977083, - "weight": 4.334420580848781e-05 - }, - { - "days": 2.978472, - "weight": 4.9718353721500726e-05 - }, - { - "days": 2.982639, - "weight": 4.9718353721500726e-05 - }, - { - "days": 2.984722, - "weight": 0.00018739994864257965 - }, - { - "days": 2.985417, - "weight": 3.059590998246198e-05 - }, - { - "days": 2.986806, - "weight": 3.5695228312872314e-05 - }, - { - "days": 2.9875, - "weight": 0.00034802847605050507 - }, - { - "days": 2.990972, - "weight": 0.0005787726305015726 - }, - { - "days": 2.993056, - "weight": 0.00043726654683268586 - }, - { - "days": 2.995139, - "weight": 0.0004053958072676213 - }, - { - "days": 2.997917, - "weight": 0.00017210199365134867 - }, - { - "days": 3.0, - "weight": 0.00032763120272886375 - }, - { - "days": 3.008333, - "weight": 0.0017222947660960892 - }, - { - "days": 3.011111, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.015278, - "weight": 6.246664954752656e-05 - }, - { - "days": 3.017361, - "weight": 4.334420580848781e-05 - }, - { - "days": 3.030556, - "weight": 4.9718353721500726e-05 - }, - { - "days": 3.031944, - "weight": 3.69700578954749e-05 - }, - { - "days": 3.032639, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.033333, - "weight": 0.00012875778784286084 - }, - { - "days": 3.036111, - "weight": 0.00010198636660820662 - }, - { - "days": 3.0375, - "weight": 4.0794546643282644e-05 - }, - { - "days": 3.038194, - "weight": 0.00022564483612065714 - }, - { - "days": 3.042361, - "weight": 0.0004665876272325453 - }, - { - "days": 3.04375, - "weight": 6.246664954752656e-05 - }, - { - "days": 3.048611, - "weight": 0.00034802847605050507 - }, - { - "days": 3.049306, - "weight": 9.943670744300145e-05 - }, - { - "days": 3.050694, - "weight": 0.0002434924502770933 - }, - { - "days": 3.052083, - "weight": 2.2946932486846487e-05 - }, - { - "days": 3.055556, - "weight": 4.9718353721500726e-05 - }, - { - "days": 3.056944, - "weight": 6.62911382953343e-05 - }, - { - "days": 3.059722, - "weight": 0.0002906611448333889 - }, - { - "days": 3.061111, - "weight": 0.00016445301615573317 - }, - { - "days": 3.090972, - "weight": 6.62911382953343e-05 - }, - { - "days": 3.095139, - "weight": 2.4221762069449073e-05 - }, - { - "days": 3.098611, - "weight": 2.677142123465424e-05 - }, - { - "days": 3.104167, - "weight": 3.69700578954749e-05 - }, - { - "days": 3.105556, - "weight": 3.059590998246198e-05 - }, - { - "days": 3.106944, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.107639, - "weight": 0.00013513193575587378 - }, - { - "days": 3.109028, - "weight": 4.206937622588523e-05 - }, - { - "days": 3.1125, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.114583, - "weight": 0.00010708568493861695 - }, - { - "days": 3.115278, - "weight": 5.099318330410331e-05 - }, - { - "days": 3.115972, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.116667, - "weight": 5.354284246930848e-05 - }, - { - "days": 3.123611, - "weight": 5.354284246930848e-05 - }, - { - "days": 3.138194, - "weight": 2.4221762069449073e-05 - }, - { - "days": 3.15, - "weight": 2.677142123465424e-05 - }, - { - "days": 3.156944, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.157639, - "weight": 3.314556914766715e-05 - }, - { - "days": 3.165278, - "weight": 5.864216079971881e-05 - }, - { - "days": 3.169444, - "weight": 3.9519717060680065e-05 - }, - { - "days": 3.170139, - "weight": 3.314556914766715e-05 - }, - { - "days": 3.174306, - "weight": 2.677142123465424e-05 - }, - { - "days": 3.175, - "weight": 5.736733121711622e-05 - }, - { - "days": 3.177778, - "weight": 2.2946932486846487e-05 - }, - { - "days": 3.18125, - "weight": 9.816187786039887e-05 - }, - { - "days": 3.213194, - "weight": 0.00013130744700806603 - }, - { - "days": 3.224306, - "weight": 7.776460453875755e-05 - }, - { - "days": 3.23125, - "weight": 4.334420580848781e-05 - }, - { - "days": 3.233333, - "weight": 2.677142123465424e-05 - }, - { - "days": 3.234028, - "weight": 3.824488747807748e-05 - }, - { - "days": 3.235417, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.236111, - "weight": 2.2946932486846487e-05 - }, - { - "days": 3.275, - "weight": 4.0794546643282644e-05 - }, - { - "days": 3.2875, - "weight": 7.776460453875755e-05 - }, - { - "days": 3.291667, - "weight": 2.4221762069449073e-05 - }, - { - "days": 3.293056, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.297917, - "weight": 2.2946932486846487e-05 - }, - { - "days": 3.300694, - "weight": 9.306255952998854e-05 - }, - { - "days": 3.343056, - "weight": 4.716869455629556e-05 - }, - { - "days": 3.347222, - "weight": 3.824488747807748e-05 - }, - { - "days": 3.349306, - "weight": 4.0794546643282644e-05 - }, - { - "days": 3.359028, - "weight": 2.677142123465424e-05 - }, - { - "days": 3.404167, - "weight": 2.9321080399859404e-05 - }, - { - "days": 3.406944, - "weight": 2.4221762069449073e-05 - }, - { - "days": 3.409028, - "weight": 2.804625081725682e-05 - }, - { - "days": 3.417361, - "weight": 7.011562704314205e-05 - }, - { - "days": 3.419444, - "weight": 3.69700578954749e-05 - }, - { - "days": 3.478472, - "weight": 3.5695228312872314e-05 - }, - { - "days": 3.479167, - "weight": 2.677142123465424e-05 - }, - { - "days": 3.485417, - "weight": 3.4420398730269734e-05 - }, - { - "days": 3.524306, - "weight": 2.4221762069449073e-05 - }, - { - "days": 3.533333, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.544444, - "weight": 3.824488747807748e-05 - }, - { - "days": 3.545139, - "weight": 6.756596787793689e-05 - }, - { - "days": 3.590278, - "weight": 3.187073956506457e-05 - }, - { - "days": 3.611111, - "weight": 7.011562704314205e-05 - }, - { - "days": 3.660417, - "weight": 3.314556914766715e-05 - }, - { - "days": 3.670139, - "weight": 0.00012748295826025828 - }, - { - "days": 3.719444, - "weight": 4.206937622588523e-05 - }, - { - "days": 3.723611, - "weight": 2.4221762069449073e-05 - }, - { - "days": 3.727778, - "weight": 0.00015042989074710476 - }, - { - "days": 3.757639, - "weight": 2.9321080399859404e-05 - }, - { - "days": 3.767361, - "weight": 3.314556914766715e-05 - }, - { - "days": 3.779167, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.782639, - "weight": 2.2946932486846487e-05 - }, - { - "days": 3.784722, - "weight": 3.314556914766715e-05 - }, - { - "days": 3.786111, - "weight": 0.00016955233448614351 - }, - { - "days": 3.798611, - "weight": 2.1672102904243905e-05 - }, - { - "days": 3.804861, - "weight": 2.4221762069449073e-05 - }, - { - "days": 3.809028, - "weight": 3.4420398730269734e-05 - }, - { - "days": 3.822222, - "weight": 3.69700578954749e-05 - }, - { - "days": 3.831944, - "weight": 3.187073956506457e-05 - }, - { - "days": 3.839583, - "weight": 2.4221762069449073e-05 - }, - { - "days": 3.840278, - "weight": 2.5496591652051656e-05 - }, - { - "days": 3.849306, - "weight": 0.00033018086189406893 - }, - { - "days": 3.857639, - "weight": 3.5695228312872314e-05 - }, - { - "days": 3.86875, - "weight": 2.804625081725682e-05 - }, - { - "days": 3.884028, - "weight": 3.4420398730269734e-05 - }, - { - "days": 3.89375, - "weight": 3.5695228312872314e-05 - }, - { - "days": 3.898611, - "weight": 0.0006463385983795095 - }, - { - "days": 3.899306, - "weight": 2.4221762069449073e-05 - }, - { - "days": 3.927083, - "weight": 6.374147913012914e-05 - }, - { - "days": 3.930556, - "weight": 4.844352413889815e-05 - }, - { - "days": 3.931944, - "weight": 0.00013640676533847635 - }, - { - "days": 3.932639, - "weight": 4.334420580848781e-05 - }, - { - "days": 3.943056, - "weight": 5.226801288670589e-05 - }, - { - "days": 3.94375, - "weight": 2.804625081725682e-05 - }, - { - "days": 3.945833, - "weight": 4.844352413889815e-05 - }, - { - "days": 3.952083, - "weight": 2.677142123465424e-05 - }, - { - "days": 3.952778, - "weight": 8.668841161697562e-05 - }, - { - "days": 3.953472, - "weight": 3.9519717060680065e-05 - }, - { - "days": 3.95625, - "weight": 3.059590998246198e-05 - }, - { - "days": 3.959722, - "weight": 9.433738911259112e-05 - }, - { - "days": 3.961111, - "weight": 3.059590998246198e-05 - }, - { - "days": 3.963889, - "weight": 6.246664954752656e-05 - }, - { - "days": 3.965278, - "weight": 0.000308508758989825 - }, - { - "days": 3.968056, - "weight": 2.804625081725682e-05 - }, - { - "days": 3.984028, - "weight": 2.804625081725682e-05 - }, - { - "days": 3.99375, - "weight": 0.00012238363992984793 - }, - { - "days": 3.995833, - "weight": 6.501630871273172e-05 - }, - { - "days": 3.996528, - "weight": 4.716869455629556e-05 - }, - { - "days": 3.998611, - "weight": 6.246664954752656e-05 - }, - { - "days": 4.001389, - "weight": 0.00015170472032970735 - }, - { - "days": 4.002778, - "weight": 2.804625081725682e-05 - }, - { - "days": 4.004167, - "weight": 3.5695228312872314e-05 - }, - { - "days": 4.00625, - "weight": 2.804625081725682e-05 - }, - { - "days": 4.007639, - "weight": 5.481767205191106e-05 - }, - { - "days": 4.008333, - "weight": 2.5496591652051656e-05 - }, - { - "days": 4.011806, - "weight": 0.00010198636660820662 - }, - { - "days": 4.015278, - "weight": 4.9718353721500726e-05 - }, - { - "days": 4.018056, - "weight": 4.334420580848781e-05 - }, - { - "days": 4.01875, - "weight": 4.9718353721500726e-05 - }, - { - "days": 4.019444, - "weight": 0.00017082716406874608 - }, - { - "days": 4.020833, - "weight": 8.796324119957821e-05 - }, - { - "days": 4.021528, - "weight": 3.5695228312872314e-05 - }, - { - "days": 4.025, - "weight": 0.00013640676533847635 - }, - { - "days": 4.029861, - "weight": 2.677142123465424e-05 - }, - { - "days": 4.035417, - "weight": 2.9321080399859404e-05 - }, - { - "days": 4.047917, - "weight": 4.206937622588523e-05 - }, - { - "days": 4.054167, - "weight": 2.804625081725682e-05 - }, - { - "days": 4.059722, - "weight": 4.0794546643282644e-05 - }, - { - "days": 4.063194, - "weight": 6.374147913012914e-05 - }, - { - "days": 4.069444, - "weight": 2.1672102904243905e-05 - }, - { - "days": 4.070139, - "weight": 5.226801288670589e-05 - }, - { - "days": 4.074306, - "weight": 6.374147913012914e-05 - }, - { - "days": 4.078472, - "weight": 2.4221762069449073e-05 - }, - { - "days": 4.079861, - "weight": 3.059590998246198e-05 - }, - { - "days": 4.080556, - "weight": 2.4221762069449073e-05 - }, - { - "days": 4.082639, - "weight": 9.56122186951937e-05 - }, - { - "days": 4.095833, - "weight": 2.2946932486846487e-05 - }, - { - "days": 4.11875, - "weight": 2.4221762069449073e-05 - }, - { - "days": 4.127083, - "weight": 4.5893864973692975e-05 - }, - { - "days": 4.129167, - "weight": 3.059590998246198e-05 - }, - { - "days": 4.131944, - "weight": 3.9519717060680065e-05 - }, - { - "days": 4.1375, - "weight": 3.187073956506457e-05 - }, - { - "days": 4.148611, - "weight": 6.62911382953343e-05 - }, - { - "days": 4.181944, - "weight": 3.69700578954749e-05 - }, - { - "days": 4.209028, - "weight": 2.4221762069449073e-05 - }, - { - "days": 4.211806, - "weight": 2.677142123465424e-05 - }, - { - "days": 4.23125, - "weight": 3.824488747807748e-05 - }, - { - "days": 4.268056, - "weight": 2.2946932486846487e-05 - }, - { - "days": 4.273611, - "weight": 2.5496591652051656e-05 - }, - { - "days": 4.305556, - "weight": 2.2946932486846487e-05 - }, - { - "days": 4.334722, - "weight": 7.521494537355238e-05 - }, - { - "days": 4.438889, - "weight": 2.804625081725682e-05 - }, - { - "days": 4.450694, - "weight": 2.1672102904243905e-05 - }, - { - "days": 4.467361, - "weight": 2.5496591652051656e-05 - }, - { - "days": 4.619444, - "weight": 3.824488747807748e-05 - }, - { - "days": 4.731944, - "weight": 2.4221762069449073e-05 - }, - { - "days": 4.760417, - "weight": 2.5496591652051656e-05 - }, - { - "days": 4.770833, - "weight": 0.00015680403866011767 - }, - { - "days": 4.801389, - "weight": 2.4221762069449073e-05 - }, - { - "days": 4.827083, - "weight": 0.00011983398076464277 - }, - { - "days": 4.884722, - "weight": 0.00020397273321641325 - }, - { - "days": 4.886806, - "weight": 2.2946932486846487e-05 - }, - { - "days": 4.900694, - "weight": 2.804625081725682e-05 - }, - { - "days": 4.940278, - "weight": 2.2946932486846487e-05 - }, - { - "days": 4.942361, - "weight": 5.354284246930848e-05 - }, - { - "days": 4.95, - "weight": 7.776460453875755e-05 - }, - { - "days": 4.951389, - "weight": 0.00020397273321641325 - }, - { - "days": 4.952083, - "weight": 2.2946932486846487e-05 - }, - { - "days": 4.952778, - "weight": 2.2946932486846487e-05 - }, - { - "days": 4.963889, - "weight": 3.314556914766715e-05 - }, - { - "days": 4.970139, - "weight": 0.00013130744700806603 - }, - { - "days": 4.978472, - "weight": 2.804625081725682e-05 - }, - { - "days": 4.9875, - "weight": 5.226801288670589e-05 - }, - { - "days": 4.99375, - "weight": 2.5496591652051656e-05 - }, - { - "days": 5.0, - "weight": 2.1672102904243905e-05 - }, - { - "days": 5.00625, - "weight": 2.677142123465424e-05 - }, - { - "days": 5.007639, - "weight": 3.4420398730269734e-05 - }, - { - "days": 5.011806, - "weight": 3.4420398730269734e-05 - }, - { - "days": 5.016667, - "weight": 3.314556914766715e-05 - }, - { - "days": 5.018056, - "weight": 8.286392286916788e-05 - }, - { - "days": 5.019444, - "weight": 5.481767205191106e-05 - }, - { - "days": 5.027778, - "weight": 2.804625081725682e-05 - }, - { - "days": 5.03125, - "weight": 2.5496591652051656e-05 - }, - { - "days": 5.054861, - "weight": 5.736733121711622e-05 - }, - { - "days": 5.078472, - "weight": 3.9519717060680065e-05 - }, - { - "days": 5.086111, - "weight": 2.804625081725682e-05 - }, - { - "days": 5.095833, - "weight": 3.059590998246198e-05 - }, - { - "days": 5.097917, - "weight": 2.2946932486846487e-05 - }, - { - "days": 5.103472, - "weight": 4.9718353721500726e-05 - }, - { - "days": 5.114583, - "weight": 2.677142123465424e-05 - }, - { - "days": 5.13125, - "weight": 2.5496591652051656e-05 - }, - { - "days": 5.771528, - "weight": 4.9718353721500726e-05 - }, - { - "days": 5.838194, - "weight": 7.39401157909498e-05 - }, - { - "days": 5.891667, - "weight": 3.059590998246198e-05 - }, - { - "days": 5.91875, - "weight": 0.00011728432159943762 - }, - { - "days": 5.936111, - "weight": 3.824488747807748e-05 - }, - { - "days": 5.947917, - "weight": 2.4221762069449073e-05 - }, - { - "days": 5.986111, - "weight": 6.119181996492397e-05 - }, - { - "days": 5.997222, - "weight": 3.059590998246198e-05 - }, - { - "days": 5.998611, - "weight": 2.5496591652051656e-05 - }, - { - "days": 6.002778, - "weight": 3.824488747807748e-05 - }, - { - "days": 6.00625, - "weight": 3.4420398730269734e-05 - }, - { - "days": 6.017361, - "weight": 3.187073956506457e-05 - }, - { - "days": 6.04375, - "weight": 4.4619035391090396e-05 - }, - { - "days": 6.124306, - "weight": 3.059590998246198e-05 - }, - { - "days": 6.765972, - "weight": 2.9321080399859404e-05 - }, - { - "days": 6.844444, - "weight": 5.354284246930848e-05 - }, - { - "days": 6.902083, - "weight": 5.481767205191106e-05 - }, - { - "days": 6.947917, - "weight": 2.5496591652051656e-05 - }, - { - "days": 6.963194, - "weight": 5.736733121711622e-05 - }, - { - "days": 6.970833, - "weight": 2.804625081725682e-05 - }, - { - "days": 7.010417, - "weight": 4.4619035391090396e-05 - }, - { - "days": 7.017361, - "weight": 3.69700578954749e-05 - }, - { - "days": 7.018056, - "weight": 2.2946932486846487e-05 - }, - { - "days": 7.051389, - "weight": 2.677142123465424e-05 - }, - { - "days": 7.965278, - "weight": 2.5496591652051656e-05 - }, - { - "days": 9.828472, - "weight": 7.011562704314205e-05 - }, - { - "days": 9.863889, - "weight": 3.69700578954749e-05 - }, - { - "days": 9.931944, - "weight": 7.266528620834722e-05 - }, - { - "days": 9.985417, - "weight": 3.187073956506457e-05 - }, - { - "days": 10.048611, - "weight": 2.9321080399859404e-05 - }, - { - "days": 10.845833, - "weight": 2.804625081725682e-05 - }, - { - "days": 10.914583, - "weight": 2.4221762069449073e-05 - }, - { - "days": 11.076389, - "weight": 4.9718353721500726e-05 - }, - { - "days": 11.150694, - "weight": 2.4221762069449073e-05 - }, - { - "days": 11.209722, - "weight": 3.824488747807748e-05 - }, - { - "days": 11.26875, - "weight": 3.4420398730269734e-05 - }, - { - "days": 11.844444, - "weight": 2.9321080399859404e-05 - }, - { - "days": 11.904861, - "weight": 2.804625081725682e-05 - }, - { - "days": 12.906944, - "weight": 3.059590998246198e-05 - }, - { - "days": 16.880556, - "weight": 3.187073956506457e-05 - }, - { - "days": 17.958333, - "weight": 3.824488747807748e-05 - }, - { - "days": 22.287131, - "weight": 0.012499999999999999 - }, - { - "days": 25.1029, - "weight": 0.012499999999999999 - }, - { - "days": 28.274415, - "weight": 0.012499999999999999 - }, - { - "days": 31.846621, - "weight": 0.012499999999999999 - }, - { - "days": 35.870141, - "weight": 0.012499999999999999 - }, - { - "days": 40.401996, - "weight": 0.012499999999999999 - }, - { - "days": 45.506408, - "weight": 0.012499999999999999 - }, - { - "days": 51.255714, - "weight": 0.012499999999999999 - }, - { - "days": 57.73139, - "weight": 0.012499999999999999 - }, - { - "days": 65.025208, - "weight": 0.012499999999999999 - }, - { - "days": 73.240531, - "weight": 0.012499999999999999 - }, - { - "days": 82.493782, - "weight": 0.012499999999999999 - }, - { - "days": 92.916094, - "weight": 0.012499999999999999 - }, - { - "days": 104.655168, - "weight": 0.012499999999999999 - }, - { - "days": 117.877362, - "weight": 0.012499999999999999 - }, - { - "days": 132.770057, - "weight": 0.012499999999999999 - }, - { - "days": 149.544303, - "weight": 0.012499999999999999 - }, - { - "days": 168.437818, - "weight": 0.012499999999999999 - }, - { - "days": 189.71835, - "weight": 0.012499999999999999 - }, - { - "days": 213.687476, - "weight": 0.012499999999999999 - }, - { - "days": 240.684877, - "weight": 0.012499999999999999 - }, - { - "days": 271.093144, - "weight": 0.012499999999999999 - }, - { - "days": 305.34321, - "weight": 0.012499999999999999 - }, - { - "days": 343.920448, - "weight": 0.012499999999999999 - }, - { - "new_client": true, - "weight": 0.015466232496134534 - } - ] -} \ No newline at end of file diff --git a/tools/DeltaIndexTestTool/make_distribution.py b/tools/DeltaIndexTestTool/make_distribution.py deleted file mode 100644 index 042152310e..0000000000 --- a/tools/DeltaIndexTestTool/make_distribution.py +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -""" -make_distribution.py - Convert a telemetry export into an analyze.py distribution JSON. - -Input is a CSV export of update-check events with the columns: - - "series name","x","y" - -where `x` is the age in days of the client's previous index version at the moment -of the event, and `y` is the number of events observed at that age. An `x` of -1 -means the client had no previous version, i.e. a net-new client that must download -a full baseline regardless of schedule. - -Because each row already counts *events*, the resulting weights are a distribution -over download events -- exactly what analyze.py's cost model expects. No additional -weighting by update frequency is applied. - -Output is the JSON format analyze.py consumes: - - { - "description": "...", - "buckets": [ - { "days": 0.5, "weight": 0.21 }, - { "new_client": true, "weight": 0.022 } - ] - } - -TRUNCATED EXPORTS ------------------ -Exports that keep only the top N buckets by event count systematically discard the -stale tail. Ages are floating-point differences between individual index publications -(a few hours apart), so frequent updaters pile onto a handful of shared ages while a -client returning after 40 days lands on a nearly unique age. Those rare ages fall -below the export's cutoff and disappear entirely -- not because such clients are rare, -but because their events are spread thin. - -The symptom is a sharp floor in the per-bucket counts: no bucket has fewer than some -value, and the bucket count is a round number. --estimate-tail detects that floor, -fits a power law to the age bands that sit comfortably above it, and reinstates the -predicted missing mass as synthetic tail buckets. - -Note for interpretation: any client staler than the refresh period must take a full -baseline, exactly like a net-new client. Missing tail mass therefore acts as a -roughly constant tax on every candidate period -- it lowers the predicted savings -without greatly moving the optimal interval. - -Usage: - python make_distribution.py --csv export.csv --output distribution.json - python make_distribution.py --csv export.csv --output distribution.json --estimate-tail - python make_distribution.py --csv export.csv --output distribution.json --tail-fraction 0.15 -""" - -import argparse -import csv -import json -import math -import sys -from collections import defaultdict - - -def load_events(csv_path): - """Return (staleness_events, new_client_count). - - staleness_events is a list of (days, count) with days >= 0. - """ - staleness = [] - new_clients = 0.0 - with open(csv_path, newline="", encoding="utf-8-sig") as f: - for row in csv.DictReader(f): - try: - x = float(row["x"]) - y = float(row["y"]) - except (KeyError, TypeError, ValueError): - continue - if y <= 0: - continue - if x < 0: - new_clients += y - else: - staleness.append((x, y)) - return staleness, new_clients - - -def detect_truncation(staleness): - """Return (floor, is_truncated) for the export's per-bucket count floor. - - A 'top N buckets' export leaves a hard floor: the smallest surviving bucket - count sits well above 1, and many buckets cluster just above it. - """ - if not staleness: - return 0.0, False - counts = sorted(c for _, c in staleness) - floor = counts[0] - near_floor = sum(1 for c in counts if c < floor * 1.5) - return floor, floor > 5 and near_floor >= 0.05 * len(counts) - - -def estimate_tail(staleness, floor, max_days, clean_factor=4.0): - """Estimate mass lost to top-N truncation and return it as (days, count) buckets. - - Fits density(D) = C * D**k over geometric age bands whose mean bucket count is at - least `clean_factor` times the truncation floor -- those bands are essentially - unaffected by the cut -- then extrapolates out to `max_days` and reinstates the - difference between predicted and observed mass. - """ - if not staleness: - return [], None - - edges = [] - e = 0.25 - while e < max_days: - edges.append(e) - e *= 1.5 - edges.append(max_days) - - bands = [] - for lo, hi in zip(edges, edges[1:]): - sel = [c for d, c in staleness if lo <= d < hi] - if sel: - bands.append((math.sqrt(lo * hi), sum(sel) / (hi - lo), sum(sel) / len(sel))) - - clean = [(mid, dens) for mid, dens, mean_count in bands if mean_count >= clean_factor * floor] - if len(clean) < 3: - return [], None - - n = len(clean) - sx = sum(math.log(d) for d, _ in clean) - sy = sum(math.log(v) for _, v in clean) - sxx = sum(math.log(d) ** 2 for d, _ in clean) - sxy = sum(math.log(d) * math.log(v) for d, v in clean) - denom = n * sxx - sx * sx - if abs(denom) < 1e-12: - return [], None - k = (n * sxy - sx * sy) / denom - c = math.exp((sy - k * sx) / n) - if k >= -1.0: - # Flatter than 1/D: the integral diverges and the fit cannot be trusted - # to extrapolate. Refuse rather than invent an enormous tail. - return [], (c, k) - - def predicted(a, b): - return c * (b ** (k + 1) - a ** (k + 1)) / (k + 1) - - start = max(mid for mid, _, _ in bands if mid <= max(m for m, _ in clean)) - tail = [] - lo = start - while lo < max_days: - hi = min(lo * 1.5, max_days) - observed = sum(cnt for d, cnt in staleness if lo <= d < hi) - missing = predicted(lo, hi) - observed - if missing > 0: - tail.append((math.sqrt(lo * hi), missing)) - lo = hi - return tail, (c, k) - - -def spread_tail(mass, min_days, max_days, steps=24): - """Distribute `mass` events log-uniformly across [min_days, max_days]. - - Spreading matters: concentrating the assumed tail at a single age makes every - refresh period shorter than that age pay the full cost and every longer period - amortize it, which manufactures a spurious optimum right at the chosen age. - """ - if mass <= 0 or max_days <= min_days: - return [] - ratio = (max_days / min_days) ** (1.0 / steps) - edges = [min_days * ratio ** i for i in range(steps + 1)] - per = mass / steps - return [(math.sqrt(lo * hi), per) for lo, hi in zip(edges, edges[1:])] - - -def bin_events(staleness, bin_days): - """Aggregate (days, count) pairs into bins. - - Each bin is represented by its count-weighted mean age, which keeps the - expectation of min(D, P) exact for every bin that does not straddle P. - A bin_days of 0 disables binning and keeps every distinct age. - """ - if bin_days <= 0: - merged = defaultdict(float) - for days, count in staleness: - merged[days] += count - return sorted(merged.items()) - - sums = defaultdict(float) - counts = defaultdict(float) - for days, count in staleness: - key = int(days / bin_days) - sums[key] += days * count - counts[key] += count - return [(sums[k] / counts[k], counts[k]) for k in sorted(counts)] - - -def main(): - parser = argparse.ArgumentParser( - description="Convert a telemetry export into an analyze.py distribution JSON.") - parser.add_argument("--csv", required=True, help="Telemetry export CSV path") - parser.add_argument("--output", required=True, help="Destination JSON path") - parser.add_argument("--bin-days", type=float, default=0.0, dest="bin_days", - help="Bin width in days for aggregating ages. " - "0 (default) keeps every distinct age, which is exact.") - parser.add_argument("--description", default=None, - help="Description recorded in the JSON. Defaults to a generated one.") - parser.add_argument("--estimate-tail", action="store_true", dest="estimate_tail", - help="Detect top-N truncation and reinstate the missing stale tail " - "by power-law extrapolation. See the module docstring.") - parser.add_argument("--tail-fraction", type=float, default=None, dest="tail_fraction", - help="Instead of estimating, assert that this fraction (0-1) of all " - "events are staler than the export shows. Overrides --estimate-tail.") - parser.add_argument("--tail-min-days", type=float, default=21.0, dest="tail_min_days", - help="Youngest age for --tail-fraction mass. Default 21.") - parser.add_argument("--tail-max-days", type=float, default=365.0, dest="tail_max_days", - help="Upper age limit when extrapolating with --estimate-tail. Default 365.") - args = parser.parse_args() - - staleness, new_clients = load_events(args.csv) - if not staleness and not new_clients: - print("Error: no usable events found in CSV.", file=sys.stderr) - sys.exit(1) - - floor, truncated = detect_truncation(staleness) - observed_total = sum(c for _, c in staleness) + new_clients - - tail = [] - fit = None - if args.tail_fraction is not None: - if not 0.0 <= args.tail_fraction < 1.0: - print("Error: --tail-fraction must be in [0, 1).", file=sys.stderr) - sys.exit(1) - if args.tail_fraction > 0: - # Mass m such that m / (observed + m) == tail_fraction. - mass = observed_total * args.tail_fraction / (1.0 - args.tail_fraction) - # Spread log-uniformly rather than lumping at one age: a point mass creates - # an artificial cliff at that age, because periods longer than it suddenly - # start amortizing those clients and a spurious second optimum appears. - tail = spread_tail(mass, args.tail_min_days, args.tail_max_days) - elif args.estimate_tail: - tail, fit = estimate_tail(staleness, floor, args.tail_max_days) - if not tail: - print("Warning: could not fit a usable tail; emitting the observed data unchanged.", - file=sys.stderr) - - binned = bin_events(staleness, args.bin_days) + [(d, c) for d, c in tail] - binned.sort() - total = sum(c for _, c in binned) + new_clients - - buckets = [{"days": round(days, 6), "weight": count / total} for days, count in binned] - if new_clients > 0: - buckets.append({"new_client": True, "weight": new_clients / total}) - - max_age = max((d for d, _ in binned), default=0.0) - tail_mass = sum(c for _, c in tail) - description = args.description or ( - f"Telemetry-derived from {args.csv}: {total:,.0f} download events, " - f"{100.0 * new_clients / total:.2f}% net-new clients, " - f"observed ages 0-{max(d for d, _ in staleness):.1f} days" - + (f", plus {100.0 * tail_mass / total:.1f}% reinstated stale tail out to " - f"{max_age:.0f} days" if tail_mass else "")) - - with open(args.output, "w", encoding="utf-8") as f: - json.dump({"description": description, "buckets": buckets}, f, indent=2) - - mean_age = sum(d * c for d, c in binned) / sum(c for _, c in binned) if binned else 0.0 - print(f"Wrote {args.output}") - print(f" Events: {total:,.0f}") - print(f" Buckets: {len(buckets)}") - print(f" New clients: {100.0 * new_clients / total:.2f}%") - print(f" Mean age: {mean_age:.3f} days") - print(f" Max age: {max_age:.2f} days") - if truncated: - print(f" NOTE: export looks truncated -- no bucket below {floor:.0f} events. " - f"The stale tail is under-counted.") - if not tail_mass: - print(f" Re-run with --estimate-tail or --tail-fraction to model it.") - if fit: - print(f" Tail fit: density = {fit[0]:.0f} * D^{fit[1]:.3f}") - if tail_mass: - print(f" Tail added: {tail_mass:,.0f} events ({100.0 * tail_mass / total:.2f}% of total)") - - -if __name__ == "__main__": - main() From fec19b798a875e01244b6c34eb6bc841ca06f7a9 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Wed, 2 Sep 2026 12:10:23 -0700 Subject: [PATCH 15/36] Required SQL builder additions --- src/AppInstallerCLITests/SQLiteWrapper.cpp | 203 ++++++++++++++++++ .../Public/winget/SQLiteStatementBuilder.h | 30 +++ .../SQLiteStatementBuilder.cpp | 55 ++++- 3 files changed, 287 insertions(+), 1 deletion(-) diff --git a/src/AppInstallerCLITests/SQLiteWrapper.cpp b/src/AppInstallerCLITests/SQLiteWrapper.cpp index 10fd960372..3a78a7dcde 100644 --- a/src/AppInstallerCLITests/SQLiteWrapper.cpp +++ b/src/AppInstallerCLITests/SQLiteWrapper.cpp @@ -730,6 +730,209 @@ TEST_CASE("SQLBuilder_InsertValueBinding", "[sqlbuilder]") } } +TEST_CASE("SQLBuilder_AssignValueNull", "[sqlbuilder]") +{ + Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); + + CreateSimpleTestTable(connection); + InsertIntoSimpleTestTable(connection, 1, "value"); + + { + INFO("Equals(nullptr) remains blocked as a filter"); + Builder::StatementBuilder builder; + REQUIRE_THROWS_HR(builder.Select(s_firstColumn).From(s_tableName).Where(s_secondColumn).Equals(nullptr), E_NOTIMPL); + } + + { + INFO("AssignValue(nullptr) assigns NULL in an update"); + Builder::StatementBuilder update; + update.Update(s_tableName).Set().Column(s_secondColumn).AssignValue(nullptr).Where(s_firstColumn).Equals(1); + update.Execute(connection); + } + + { + INFO("The value is now NULL"); + Builder::StatementBuilder select; + select.Select({ s_firstColumn, s_secondColumn }).From(s_tableName); + + Statement statement = select.Prepare(connection); + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == 1); + REQUIRE(statement.GetColumnIsNull(1)); + REQUIRE(!statement.Step()); + } +} + +TEST_CASE("SQLBuilder_CreateTempView", "[sqlbuilder]") +{ + Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); + + CreateSimpleTestTable(connection); + InsertIntoSimpleTestTable(connection, 1, "one"); + InsertIntoSimpleTestTable(connection, 2, "two"); + + constexpr std::string_view viewName = "simpleview"; + + { + // Note that SQLite prohibits bound parameters in a view definition, so the + // statement that defines a view must be structural only. + INFO("Create a view over the table"); + Builder::StatementBuilder createView; + createView.CreateTempView(viewName).Select({ s_firstColumn, s_secondColumn }).From(s_tableName).OrderBy(s_firstColumn); + createView.Execute(connection); + } + + { + INFO("The view returns the underlying rows"); + Builder::StatementBuilder select; + select.Select({ s_firstColumn, s_secondColumn }).From(viewName); + + Statement statement = select.Prepare(connection); + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == 1); + REQUIRE(statement.GetColumn(1) == "one"); + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == 2); + REQUIRE(statement.GetColumn(1) == "two"); + REQUIRE(!statement.Step()); + } + + { + INFO("A filter can still be applied when reading the view"); + Builder::StatementBuilder select; + select.Select(s_secondColumn).From(viewName).Where(s_firstColumn).Equals(2); + + Statement statement = select.Prepare(connection); + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == "two"); + REQUIRE(!statement.Step()); + } +} + +TEST_CASE("SQLBuilder_UnionAll", "[sqlbuilder]") +{ + Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); + + CreateSimpleTestTable(connection); + InsertIntoSimpleTestTable(connection, 1, "one"); + InsertIntoSimpleTestTable(connection, 2, "two"); + + Builder::StatementBuilder select; + select.Select(s_firstColumn).From(s_tableName).Where(s_firstColumn).Equals(1). + UnionAll(). + Select(s_firstColumn).From(s_tableName).Where(s_firstColumn).Equals(2); + + Statement statement = select.Prepare(connection); + + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == 1); + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == 2); + REQUIRE(!statement.Step()); +} + +TEST_CASE("SQLBuilder_NotExists", "[sqlbuilder]") +{ + Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); + + constexpr std::string_view otherTable = "othertest"; + + CreateSimpleTestTable(connection); + InsertIntoSimpleTestTable(connection, 1, "one"); + InsertIntoSimpleTestTable(connection, 2, "two"); + + { + Builder::StatementBuilder createTable; + createTable.CreateTable(otherTable).Columns({ Builder::ColumnBuilder(s_firstColumn, Builder::Type::Int) }); + createTable.Execute(connection); + + Builder::StatementBuilder insert; + insert.InsertInto(otherTable).Columns(s_firstColumn).Values(2); + insert.Execute(connection); + } + + // Select rows from the simple table that have no matching row in the other table. + Builder::StatementBuilder select; + select.Select(Builder::QualifiedColumn{ s_tableName, s_firstColumn }).From(s_tableName). + Where().NotExists().BeginParenthetical(). + Select(Builder::QualifiedColumn{ otherTable, s_firstColumn }).From(otherTable). + Where(Builder::QualifiedColumn{ otherTable, s_firstColumn }).Equals(Builder::QualifiedColumn{ s_tableName, s_firstColumn }). + EndParenthetical(); + + Statement statement = select.Prepare(connection); + + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == 1); + REQUIRE(!statement.Step()); +} + +TEST_CASE("SQLBuilder_AttachAndTempView", "[sqlbuilder]") +{ + TestCommon::TempFile baselineFile{ "repolibtest_baseline"s, ".db"s }; + INFO("Using temporary file named: " << baselineFile.GetPath()); + + { + INFO("Create the database that will be attached"); + Connection baseline = Connection::Create(baselineFile, Connection::OpenDisposition::Create); + CreateSimpleTestTable(baseline); + InsertIntoSimpleTestTable(baseline, 1, "baseline"); + } + + Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); + + constexpr std::string_view baselineAlias = "baseline"; + constexpr std::string_view deltaTable = "deltatest"; + + { + INFO("Create a local table with a distinct row"); + Builder::StatementBuilder createTable; + createTable.CreateTable(deltaTable).Columns({ + Builder::ColumnBuilder(s_firstColumn, Builder::Type::Int), + Builder::ColumnBuilder(s_secondColumn, Builder::Type::Text), + }); + createTable.Execute(connection); + + Builder::StatementBuilder insert; + insert.InsertInto(deltaTable).Columns({ s_firstColumn, s_secondColumn }).Values(2, "delta"); + insert.Execute(connection); + } + + { + INFO("Attach the baseline database"); + Builder::StatementBuilder attach; + attach.Attach(baselineFile.GetPath().u8string(), baselineAlias); + attach.Execute(connection); + } + + { + INFO("A temp view can span the local and attached databases"); + Builder::StatementBuilder createView; + createView.CreateTempView(s_tableName). + Select({ s_firstColumn, s_secondColumn }).From(deltaTable). + UnionAll(). + Select({ s_firstColumn, s_secondColumn }).From(Builder::QualifiedTable{ baselineAlias, s_tableName }); + createView.Execute(connection); + } + + { + INFO("Reading the view returns the merged rows"); + Builder::StatementBuilder select; + select.Select({ s_firstColumn, s_secondColumn }).From(s_tableName).OrderBy(s_firstColumn); + + Statement statement = select.Prepare(connection); + + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == 1); + REQUIRE(statement.GetColumn(1) == "baseline"); + + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == 2); + REQUIRE(statement.GetColumn(1) == "delta"); + + REQUIRE(!statement.Step()); + } +} + TEST_CASE("SQLiteWrapperTransactionRollback", "[sqlitewrapper]") { Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h index 0ef543a87a..58a90952a9 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h @@ -253,6 +253,9 @@ namespace AppInstaller::SQLite::Builder StatementBuilder& Where(std::string_view column); StatementBuilder& Where(const QualifiedColumn& column); + // Begin a filter clause that is not rooted at a column, such as one using `exists`. + StatementBuilder& Where(); + // A full filter clause looking for an embedded null character. // Is extremely specific to consistency checks, and so a more detailed construct is not required. StatementBuilder& WhereValueContainsEmbeddedNullCharacter(std::string_view column); @@ -284,6 +287,11 @@ namespace AppInstaller::SQLite::Builder StatementBuilder& Equals(); StatementBuilder& Equals(const QualifiedColumn& column); + // Assigns NULL to the current column in the `set` portion of an update statement. + // Only valid in an update; `Equals(nullptr)` is intentionally blocked because + // `value = NULL` is always false when used as a filter. Use IsNull for filtering. + StatementBuilder& AssignValue(std::nullptr_t); + template StatementBuilder& IsGreaterThan(const ValueType& value) { @@ -311,6 +319,12 @@ namespace AppInstaller::SQLite::Builder // Appends a set of value binders for the In clause. StatementBuilder& In(size_t count); + // Begins an `exists` subquery clause; follow with a parenthetical select. + StatementBuilder& Exists(); + + // Begins a `not exists` subquery clause; follow with a parenthetical select. + StatementBuilder& NotExists(); + // IsNull(true) means the value is null; IsNull(false) means the value is not null. StatementBuilder& IsNull(bool isNull = true); StatementBuilder& IsNotNull() { return IsNull(false); } @@ -437,6 +451,16 @@ namespace AppInstaller::SQLite::Builder StatementBuilder& DropTableIfExists(QualifiedTable table); StatementBuilder& DropTableIfExists(std::initializer_list table); + // Begin a temporary view creation statement, terminated by the `as` keyword. + // Temporary views live only for the duration of the connection, and are the only + // form of view that can reference tables in an attached database. + // The initializer_list form enables the view name to be constructed from multiple parts. + StatementBuilder& CreateTempView(std::string_view view); + StatementBuilder& CreateTempView(std::initializer_list view); + + // Combine the preceding select statement with the one that follows, retaining duplicate rows. + StatementBuilder& UnionAll(); + // Begin an index creation statement. // The initializer_list form enables the index name to be constructed from multiple parts. StatementBuilder& CreateIndex(std::string_view table); @@ -483,6 +507,12 @@ namespace AppInstaller::SQLite::Builder // Output the set portion of an update statement. StatementBuilder& Vacuum(); + // Attaches another database file to the connection under the given alias. + // The file path is bound as a parameter rather than embedded in the statement text. + // The alias remains valid only for the connection that executes the statement, and + // the attachment is released when that connection is closed. + StatementBuilder& Attach(const std::string& path, std::string_view alias); + // General purpose functions to begin and end a parenthetical expression. StatementBuilder& BeginParenthetical(); StatementBuilder& EndParenthetical(); diff --git a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp index 4277ebf1b5..df1cf46f38 100644 --- a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp +++ b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp @@ -324,6 +324,12 @@ namespace AppInstaller::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::Where() + { + m_stream << " WHERE"; + return *this; + } + StatementBuilder& StatementBuilder::WhereValueContainsEmbeddedNullCharacter(std::string_view column) { OutputColumns(m_stream, " WHERE instr(", column); @@ -348,10 +354,17 @@ namespace AppInstaller::SQLite::Builder { // This is almost certainly not what you want. // In SQL, value = NULL is always false. - // Use StatementBuilder::IsNull instead. + // Use StatementBuilder::IsNull instead, or StatementBuilder::AssignValue + // to assign NULL in the set portion of an update statement. THROW_HR(E_NOTIMPL); } + StatementBuilder& StatementBuilder::AssignValue(std::nullptr_t) + { + m_stream << " = NULL"; + return *this; + } + StatementBuilder& StatementBuilder::Equals() { m_stream << " ="; @@ -420,6 +433,18 @@ namespace AppInstaller::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::Exists() + { + m_stream << " EXISTS"; + return *this; + } + + StatementBuilder& StatementBuilder::NotExists() + { + m_stream << " NOT EXISTS"; + return *this; + } + StatementBuilder& StatementBuilder::IsNull(bool isNull) { m_stream << " IS " << (isNull ? "" : "NOT ") << "NULL"; @@ -776,6 +801,26 @@ namespace AppInstaller::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::CreateTempView(std::string_view view) + { + OutputOperationAndTable(m_stream, "CREATE TEMP VIEW", view); + m_stream << " AS "; + return *this; + } + + StatementBuilder& StatementBuilder::CreateTempView(std::initializer_list view) + { + OutputOperationAndTable(m_stream, "CREATE TEMP VIEW", view); + m_stream << " AS "; + return *this; + } + + StatementBuilder& StatementBuilder::UnionAll() + { + m_stream << " UNION ALL "; + return *this; + } + StatementBuilder& StatementBuilder::CreateIndex(std::string_view table) { OutputOperationAndTable(m_stream, "CREATE INDEX", table); @@ -909,6 +954,14 @@ namespace AppInstaller::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::Attach(const std::string& path, std::string_view alias) + { + m_stream << "ATTACH DATABASE ?"; + AddBindFunctor(m_bindIndex++, path); + OutputOperationAndTable(m_stream, " AS", alias); + return *this; + } + StatementBuilder& StatementBuilder::BeginParenthetical() { m_stream << '('; From 7f99a72a8a78eac50271ba72027d3bf9db7b90b8 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Wed, 2 Sep 2026 16:20:27 -0700 Subject: [PATCH 16/36] Update tracking and some minor fixes --- src/AppInstallerCLITests/SQLiteIndex.cpp | 126 +++++++++- src/AppInstallerCLITests/SQLiteWrapper.cpp | 31 +++ ...AppInstallerRepositoryCore.vcxproj.filters | 9 + .../Microsoft/Schema/2_0/Interface.h | 6 + .../Microsoft/Schema/2_0/Interface_2_0.cpp | 69 +++--- .../Schema/2_0/PackageUpdateTrackingTable.cpp | 215 ++++++++++-------- .../Schema/2_0/PackageUpdateTrackingTable.h | 34 ++- .../Microsoft/Schema/2_1/Interface.h | 5 + .../Microsoft/Schema/2_1/Interface_2_1.cpp | 19 +- .../Public/winget/SQLiteStatementBuilder.h | 3 + .../SQLiteStatementBuilder.cpp | 6 + 11 files changed, 382 insertions(+), 141 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index 90f2ae71f6..f93b728d97 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -3585,7 +3585,7 @@ TEST_CASE("SQLiteIndex_MigrateTo_Data", "[sqliteindex][V2_0]") REQUIRE(index.GetVersion() == SQLiteVersion{ 2, 0 }); Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite); - auto updateData = Schema::V2_0::PackageUpdateTrackingTable::GetUpdatesSince(connection, 0); + auto updateData = Schema::V2_0::PackageUpdateTrackingTable::GetUpdatesSince(connection, 0, Schema::V2_0::PackageUpdateTrackingTable::RemovalBehavior::Delete); REQUIRE(updateData.size() == 3); REQUIRE(std::count_if(updateData.begin(), updateData.end(), [&](const auto& x) { return x.PackageIdentifier == packageId1; }) == 1); @@ -3967,6 +3967,130 @@ TEST_CASE("SQLiteIndex_VersionStringPreserved", "[sqliteindex]") REQUIRE(extractedVersion == version); } +TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_RemovalDeletesRow", "[sqliteindex][V2_0][updatetracking]") +{ + TempFile indexFile{ "updatetracking"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + index.RemoveManifest(m2.Manifest, m2.Path); + REQUIRE(index.CheckConsistency(true)); + } + + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadWrite); + using Tracking = Schema::V2_0::PackageUpdateTrackingTable; + + // 2.0 deletes the row, so the removed package leaves no trace at all. + auto updates = Tracking::GetUpdatesSince(connection, 0, Tracking::RemovalBehavior::Delete); + REQUIRE(updates.size() == 1); + REQUIRE(updates[0].PackageIdentifier == "Publisher1.Id"); + + // Read with Record to check on the Delete + REQUIRE(Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record).empty()); +} + +TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_RemovalIsRecorded", "[sqliteindex][V2_1][updatetracking]") +{ + TempFile indexFile{ "updatetracking"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + index.RemoveManifest(m2.Manifest, m2.Path); + REQUIRE(index.CheckConsistency(true)); + } + + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadWrite); + using Tracking = Schema::V2_0::PackageUpdateTrackingTable; + + // The removal must not appear as an update; that would change what the version data + // manifest export writes out, which is 2.0 behavior that 2.1 preserves exactly. + auto updates = Tracking::GetUpdatesSince(connection, 0, Tracking::RemovalBehavior::Record); + REQUIRE(updates.size() == 1); + REQUIRE(updates[0].PackageIdentifier == "Publisher1.Id"); + + auto removals = Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record); + REQUIRE(removals.size() == 1); + REQUIRE(removals[0] == "Publisher2.Id"); +} + +TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_ReAddClearsRemoval", "[sqliteindex][V2_1][updatetracking]") +{ + TempFile indexFile{ "updatetracking"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + index.RemoveManifest(m2.Manifest, m2.Path); + index.AddManifest(m2.Manifest, m2.Path); + REQUIRE(index.CheckConsistency(true)); + } + + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadWrite); + using Tracking = Schema::V2_0::PackageUpdateTrackingTable; + + auto updates = Tracking::GetUpdatesSince(connection, 0, Tracking::RemovalBehavior::Record); + REQUIRE(updates.size() == 2); + + REQUIRE(Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record).empty()); +} + +TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_MigrateFrom_2_0", "[sqliteindex][V2_1][updatetracking]") +{ + TempFile indexFile{ "updatetracking"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 0 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + } + + { + SQLiteIndex index = SQLiteIndex::Open(indexFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + REQUIRE(index.MigrateTo(SQLiteVersion{ 2, 1 })); + REQUIRE(index.GetVersion() == SQLiteVersion{ 2, 1 }); + + // Removals recorded after migration require the column added by the migration. + index.RemoveManifest(m2.Manifest, m2.Path); + REQUIRE(index.CheckConsistency(true)); + } + + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadWrite); + using Tracking = Schema::V2_0::PackageUpdateTrackingTable; + + auto removals = Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record); + REQUIRE(removals.size() == 1); + REQUIRE(removals[0] == "Publisher2.Id"); +} + TEST_CASE("SQLiteIndex_Delta_AddedPackage", "[sqliteindex][V2_1][delta]") { TempFile workingFile{ "delta_working"s, ".db"s }; diff --git a/src/AppInstallerCLITests/SQLiteWrapper.cpp b/src/AppInstallerCLITests/SQLiteWrapper.cpp index 3a78a7dcde..a7d32aa976 100644 --- a/src/AppInstallerCLITests/SQLiteWrapper.cpp +++ b/src/AppInstallerCLITests/SQLiteWrapper.cpp @@ -763,6 +763,37 @@ TEST_CASE("SQLBuilder_AssignValueNull", "[sqlbuilder]") } } +TEST_CASE("SQLBuilder_AddColumnWithConstraints", "[sqlbuilder]") +{ + Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); + + CreateSimpleTestTable(connection); + InsertIntoSimpleTestTable(connection, 1, "one"); + + constexpr std::string_view addedColumn = "added"; + + { + // SQLite requires a non-null default when adding a column declared as not null, + // so the plain Add(column, type) form cannot express this. + INFO("Add a not null column with a default"); + Builder::StatementBuilder alter; + alter.AlterTable(s_tableName).Add(Builder::ColumnBuilder(addedColumn, Builder::Type::Int64).NotNull().Default(0)); + alter.Execute(connection); + } + + { + INFO("The existing row receives the default rather than null"); + Builder::StatementBuilder select; + select.Select(addedColumn).From(s_tableName); + + Statement statement = select.Prepare(connection); + REQUIRE(statement.Step()); + REQUIRE(!statement.GetColumnIsNull(0)); + REQUIRE(statement.GetColumn(0) == 0); + REQUIRE(!statement.Step()); + } +} + TEST_CASE("SQLBuilder_CreateTempView", "[sqlbuilder]") { Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index 64f1b186c2..2aa553dbc9 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -127,6 +127,9 @@ {b4c8d9e2-f3a5-6b7c-0d1e-2f3a4b5c6d7e} + + {b4b0b84d-5dea-42bb-817b-7cf269008f52} + @@ -519,6 +522,9 @@ Header Files + + Microsoft\Schema\2_1 + @@ -812,6 +818,9 @@ Source Files + + Microsoft\Schema\2_1 + diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h index c85603d956..95a0136788 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h @@ -4,6 +4,7 @@ #include "Microsoft/Schema/ISQLiteIndex.h" #include "Microsoft/Schema/2_0/SearchResultsTable.h" #include "Microsoft/Schema/2_0/OneToManyTableWithMap.h" +#include "Microsoft/Schema/2_0/PackageUpdateTrackingTable.h" #include #include @@ -62,6 +63,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 void SetupDeltaReadMode(SQLite::Connection& connection, const std::filesystem::path& baselinePath); protected: + // Determines how the removal of a package is recorded in the update tracking table. + // Version 2.0 deletes the row; later versions may record the removal instead so that + // a delta index can express it. + virtual PackageUpdateTrackingTable::RemovalBehavior GetTrackingRemovalBehavior() const; + // Creates the search results table. virtual std::unique_ptr CreateSearchResultsTable(const SQLite::Connection& connection) const; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index 8ae0b29bd5..00a9e4befd 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -410,7 +410,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { EnsureInternalInterface(connection, true); SQLite::rowid_t manifestId = m_internalInterface->AddManifest(connection, manifest, relativePath); - PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, manifestId, PackageVersionProperty::Id).value()); + PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, manifestId, PackageVersionProperty::Id).value(), GetTrackingRemovalBehavior()); return manifestId; } @@ -420,7 +420,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 std::pair result = m_internalInterface->UpdateManifest(connection, manifest, relativePath); if (result.first) { - PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, result.second, PackageVersionProperty::Id).value()); + PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, result.second, PackageVersionProperty::Id).value(), GetTrackingRemovalBehavior()); } return result; } @@ -446,7 +446,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 m_internalInterface->RemoveManifestById(connection, manifestId); if (identifier) { - PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), identifier.value()); + PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), identifier.value(), GetTrackingRemovalBehavior()); } } @@ -477,7 +477,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (m_internalInterface) { AICLI_CHECK_CONSISTENCY(m_internalInterface->CheckConsistency(connection, log)); - AICLI_CHECK_CONSISTENCY(PackageUpdateTrackingTable::CheckConsistency(connection, m_internalInterface.get(), log)); + AICLI_CHECK_CONSISTENCY(PackageUpdateTrackingTable::CheckConsistency(connection, m_internalInterface.get(), GetTrackingRemovalBehavior(), log)); return result; } @@ -701,14 +701,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migrate_from_v2_0"); // We only need to insert all of the existing packages into the update tracking table. - PackageUpdateTrackingTable::EnsureExists(connection); + PackageUpdateTrackingTable::EnsureExists(connection, GetTrackingRemovalBehavior()); SearchResult allPackages = current->Search(connection, {}); for (const auto& packageMatch : allPackages.Matches) { std::vector versionKeys = current->GetVersionKeysById(connection, packageMatch.first); ISQLiteIndex::VersionKey& latestVersionKey = versionKeys[0]; - PackageUpdateTrackingTable::Update(connection, current, current->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Id).value(), false); + PackageUpdateTrackingTable::Update(connection, current, current->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Id).value(), GetTrackingRemovalBehavior(), false); } savepoint.Commit(); @@ -739,6 +739,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } + PackageUpdateTrackingTable::RemovalBehavior Interface::GetTrackingRemovalBehavior() const + { + return PackageUpdateTrackingTable::RemovalBehavior::Delete; + } + std::unique_ptr Interface::CreateSearchResultsTable(const SQLite::Connection& connection) const { return std::make_unique(connection); @@ -954,16 +959,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 THROW_WIN32_IF(ERROR_INVALID_STATE, baseOutputDirectory.empty() || baseOutputDirectory.is_relative()); // TEMP - PackageUpdateTrackingTable::EnsureExists(connection); + PackageUpdateTrackingTable::EnsureExists(connection, GetTrackingRemovalBehavior()); // Output all of the changed package version manifests since the base time to the target location - for (const auto& packageData : PackageUpdateTrackingTable::GetUpdatesSince(connection, updateBaseTime)) + for (const auto& packageData : PackageUpdateTrackingTable::GetUpdatesSince(connection, updateBaseTime, GetTrackingRemovalBehavior())) { - if (packageData.IsRemoved) - { - continue; - } - std::filesystem::path packageDirectory = baseOutputDirectory / Manifest::PackageVersionDataManifest::GetRelativeDirectoryPath(packageData.PackageIdentifier, Utility::SHA256::ConvertToString(packageData.Hash)); @@ -1072,7 +1072,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 deltaUpdateBaseTime = std::stoll(deltaUpdateBaseTimeString.value()); } - auto changedPackages = PackageUpdateTrackingTable::GetUpdatesSince(connection, deltaUpdateBaseTime); + auto changedPackages = PackageUpdateTrackingTable::GetUpdatesSince(connection, deltaUpdateBaseTime, GetTrackingRemovalBehavior()); + auto removedPackages = PackageUpdateTrackingTable::GetRemovalsSince(connection, deltaUpdateBaseTime, GetTrackingRemovalBehavior()); SQLite::Connection deltaConn = SQLite::Connection::Create( deltaOutputPath.u8string(), SQLite::Connection::OpenDisposition::Create); @@ -1090,27 +1091,29 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 SQLite::Savepoint deltaSavepoint = SQLite::Savepoint::Create(deltaConn, "delta_build"); - for (const auto& pkg : changedPackages) + for (const auto& packageIdentifier : removedPackages) { - SQLite::rowid_t packageRowid = anon::GetBaselinePackageRowid(baselineConn, pkg.PackageIdentifier); + SQLite::rowid_t packageRowid = anon::GetBaselinePackageRowid(baselineConn, packageIdentifier); - if (pkg.IsRemoved) + if (packageRowid == 0) { - if (packageRowid == 0) - { - // Package was added and removed within the same tracking window; skip. - continue; - } + // Package was added and removed within the same tracking window; skip. + continue; + } - AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); + AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << packageIdentifier << "] (rowid=" << packageRowid << ")"); + + std::string sql = "INSERT OR REPLACE INTO delta_packages (rowid, id, name, latest_version, is_removed) VALUES (?, ?, '', '', 1)"; + SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); + stmt.Bind(1, packageRowid); + stmt.Bind(2, packageIdentifier); + stmt.Execute(); + } + + for (const auto& pkg : changedPackages) + { + SQLite::rowid_t packageRowid = anon::GetBaselinePackageRowid(baselineConn, pkg.PackageIdentifier); - std::string sql = "INSERT OR REPLACE INTO delta_packages (rowid, id, name, latest_version, is_removed) VALUES (?, ?, '', '', 1)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, packageRowid); - stmt.Bind(2, pkg.PackageIdentifier); - stmt.Execute(); - } - else { bool isNewPackage = (packageRowid == 0); if (isNewPackage) @@ -1170,11 +1173,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, "commands2", "command", packageRowid, nextNewCommandsRowid); } + } - deltaSavepoint.Commit(); + deltaSavepoint.Commit(); - AICLI_LOG(Repo, Info, << "Delta index generation complete"); - } + AICLI_LOG(Repo, Info, << "Delta index generation complete"); } PackagesTable::PrepareForPackaging< diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp index 52eeeefb2b..56e9f33216 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp @@ -23,7 +23,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return s_PUTT_Table_Name; } - void PackageUpdateTrackingTable::Create(SQLite::Connection& connection) + void PackageUpdateTrackingTable::Create(SQLite::Connection& connection, RemovalBehavior removals) { using namespace Builder; @@ -35,7 +35,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 builder.Column(ColumnBuilder(s_PUTT_WriteTime, Type::Int64).NotNull()); builder.Column(ColumnBuilder(s_PUTT_Manifest, Type::Blob)); builder.Column(ColumnBuilder(s_PUTT_Hash, Type::Blob)); - builder.Column(ColumnBuilder(s_PUTT_IsRemoved, Type::Int64).Default(0).NotNull()); + + if (removals == RemovalBehavior::Record) + { + builder.Column(ColumnBuilder(s_PUTT_IsRemoved, Type::Int64).NotNull().Default(0)); + } builder.EndColumns(); @@ -46,11 +50,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 indexBuilder.Execute(connection); } - void PackageUpdateTrackingTable::EnsureExists(SQLite::Connection& connection) + void PackageUpdateTrackingTable::EnsureExists(SQLite::Connection& connection, RemovalBehavior removals) { if (!Exists(connection)) { - Create(connection); + Create(connection, removals); } } @@ -73,11 +77,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return statement.GetColumn(0) != 0; } - void PackageUpdateTrackingTable::Update(SQLite::Connection& connection, const ISQLiteIndex* internalIndex, const std::string& packageIdentifier, bool ensureTable) + void PackageUpdateTrackingTable::Update(SQLite::Connection& connection, const ISQLiteIndex* internalIndex, const std::string& packageIdentifier, RemovalBehavior removals, bool ensureTable) { if (ensureTable) { - EnsureExists(connection); + EnsureExists(connection, removals); } SearchRequest request; @@ -86,26 +90,39 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (result.Matches.empty()) { - // Mark the package as removed rather than deleting the row; clear the data columns. - int64_t currentTime = Utility::GetCurrentUnixEpoch(); - - Builder::StatementBuilder updateBuilder; - updateBuilder.Update(s_PUTT_Table_Name).Set(). - Column(s_PUTT_WriteTime).Equals(currentTime). - Column(s_PUTT_Manifest).Equals(nullptr). - Column(s_PUTT_Hash).Equals(nullptr). - Column(s_PUTT_IsRemoved).Equals(1). - Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); - updateBuilder.Execute(connection); + if (removals == RemovalBehavior::Delete) + { + // Remove any existing package update row + Builder::StatementBuilder deleteBuilder; + deleteBuilder.DeleteFrom(s_PUTT_Table_Name).Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); - if (connection.GetChanges() == 0) + deleteBuilder.Execute(connection); + } + else { - // Package was never tracked (added and removed before any tracking checkpoint); record its removal. - Builder::StatementBuilder insertBuilder; - insertBuilder.InsertInto(s_PUTT_Table_Name). - Columns({ s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_IsRemoved }). - Values(packageIdentifier, currentTime, 1); - insertBuilder.Execute(connection); + // Mark the package as removed rather than deleting the row, clearing the data columns. + int64_t currentTime = Utility::GetCurrentUnixEpoch(); + + Builder::StatementBuilder updateBuilder; + updateBuilder.Update(s_PUTT_Table_Name).Set(). + Column(s_PUTT_WriteTime).Equals(currentTime). + Column(s_PUTT_Manifest).AssignValue(nullptr). + Column(s_PUTT_Hash).AssignValue(nullptr). + Column(s_PUTT_IsRemoved).Equals(1). + Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); + updateBuilder.Execute(connection); + + if (connection.GetChanges() == 0) + { + // The package was added and removed without an intervening tracking checkpoint, + // so there is no row to mark. Record the removal so that a delta built against an + // older baseline still learns that the package is gone. + Builder::StatementBuilder insertBuilder; + insertBuilder.InsertInto(s_PUTT_Table_Name). + Columns({ s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_IsRemoved }). + Values(packageIdentifier, currentTime, 1); + insertBuilder.Execute(connection); + } } } else @@ -139,14 +156,19 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 int64_t currentTime = Utility::GetCurrentUnixEpoch(); // First attempt to update the row and then insert it if no modification occurred. - // Also clears is_removed in case this package was previously removed and is being re-added. Builder::StatementBuilder updateBuilder; updateBuilder.Update(s_PUTT_Table_Name).Set(). Column(s_PUTT_WriteTime).Equals(currentTime). Column(s_PUTT_Manifest).Equals(compressedManifest). - Column(s_PUTT_Hash).Equals(manifestHash). - Column(s_PUTT_IsRemoved).Equals(0). - Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); + Column(s_PUTT_Hash).Equals(manifestHash); + + if (removals == RemovalBehavior::Record) + { + // Clear the flag in case this package was previously removed and is now being re-added. + updateBuilder.Column(s_PUTT_IsRemoved).Equals(0); + } + + updateBuilder.Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); updateBuilder.Execute(connection); @@ -162,45 +184,30 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } - bool PackageUpdateTrackingTable::CheckConsistency(const SQLite::Connection& connection, ISQLiteIndex* internalIndex, bool log) + bool PackageUpdateTrackingTable::CheckConsistency(const SQLite::Connection& connection, ISQLiteIndex* internalIndex, RemovalBehavior removals, bool log) { bool result = true; - // Ensure that all non-removed data in the update table matches the internal index - for (const PackageData& packageData : GetUpdatesSince(connection, 0)) + // Ensure that all data in the update table matches the internal index + for (const PackageData& packageData : GetUpdatesSince(connection, 0, removals)) { - if (packageData.IsRemoved) - { - // Removed packages should not be in the internal index - SearchRequest request; - request.Inclusions.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, packageData.PackageIdentifier); - if (!internalIndex->Search(connection, request).Matches.empty()) - { - if (!log) - { - return false; - } - result = false; - AICLI_LOG(Repo, Info, << " [INVALID] value [" << s_PUTT_Package << "] in table [" << s_PUTT_Table_Name << - "] at row [" << packageData.RowID << "]; package [" << packageData.PackageIdentifier << "] is marked removed but still exists in the internal index"); - } - continue; - } - - if (packageData.Manifest.empty()) + auto manifestHash = Utility::SHA256::ComputeHash(packageData.Manifest); + if (!Utility::SHA256::AreEqual(packageData.Hash, manifestHash)) { if (!log) { return false; } + result = false; - AICLI_LOG(Repo, Info, << " [INVALID] value [" << s_PUTT_Manifest << "] in table [" << s_PUTT_Table_Name << - "] at row [" << packageData.RowID << "]; manifest blob is empty for non-removed package"); - continue; + AICLI_LOG(Repo, Info, << " [INVALID] value [" << s_PUTT_Hash << "] in table [" << s_PUTT_Table_Name << + "] at row [" << packageData.RowID << "]; the hash of the manifest value does not match the hash in the row"); } - auto manifestHash = Utility::SHA256::ComputeHash(packageData.Manifest); - if (!Utility::SHA256::AreEqual(packageData.Hash, manifestHash)) + SearchRequest request; + request.Inclusions.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, packageData.PackageIdentifier); + + if (internalIndex->Search(connection, request).Matches.empty()) { if (!log) { @@ -208,14 +215,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } result = false; - AICLI_LOG(Repo, Info, << " [INVALID] value [" << s_PUTT_Hash << "] in table [" << s_PUTT_Table_Name << - "] at row [" << packageData.RowID << "]; the hash of the manifest value does not match the hash in the row"); + AICLI_LOG(Repo, Info, << " [INVALID] value [" << s_PUTT_Package << "] in table [" << s_PUTT_Table_Name << + "] at row [" << packageData.RowID << "]; the package [" << packageData.PackageIdentifier << "] was not found in the internal index"); } + } + // Any package recorded as removed must no longer be in the internal index + for (const std::string& packageIdentifier : GetRemovalsSince(connection, 0, removals)) + { SearchRequest request; - request.Inclusions.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, packageData.PackageIdentifier); + request.Inclusions.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, packageIdentifier); - if (internalIndex->Search(connection, request).Matches.empty()) + if (!internalIndex->Search(connection, request).Matches.empty()) { if (!log) { @@ -224,15 +235,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 result = false; AICLI_LOG(Repo, Info, << " [INVALID] value [" << s_PUTT_Package << "] in table [" << s_PUTT_Table_Name << - "] at row [" << packageData.RowID << "]; the package [" << packageData.PackageIdentifier << "] was not found in the internal index"); + "]; the package [" << packageIdentifier << "] is marked as removed but is present in the internal index"); } } - // Ensure that all packages in the internal index are present in the update table (as non-removed) + // Ensure that all packages in the internal index are present in the update table Builder::StatementBuilder builder; - builder.Select(Builder::RowCount).From(s_PUTT_Table_Name). - Where(s_PUTT_Package).Like(Builder::Unbound).Escape(EscapeCharForLike). - And(s_PUTT_IsRemoved).Equals(0); + builder.Select(Builder::RowCount).From(s_PUTT_Table_Name).Where(s_PUTT_Package).Like(Builder::Unbound).Escape(EscapeCharForLike); + + if (removals == RemovalBehavior::Record) + { + builder.And(s_PUTT_IsRemoved).Equals(0); + } Statement select = builder.Prepare(connection); @@ -262,12 +276,19 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return result; } - std::vector PackageUpdateTrackingTable::GetUpdatesSince(const SQLite::Connection& connection, int64_t updateBaseTime) + std::vector PackageUpdateTrackingTable::GetUpdatesSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals) { Builder::StatementBuilder builder; - builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash, s_PUTT_IsRemoved }). + builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash }). From(s_PUTT_Table_Name).Where(s_PUTT_WriteTime).IsGreaterThanOrEqualTo(updateBaseTime); + if (removals == RemovalBehavior::Record) + { + // Removals are reported separately, so that this remains the set of packages that + // have data to write out, exactly as it is when removals delete their row. + builder.And(s_PUTT_IsRemoved).Equals(0); + } + Statement select = builder.Prepare(connection); std::vector result; @@ -278,13 +299,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 item.RowID = select.GetColumn(0); item.PackageIdentifier = select.GetColumn(1); item.WriteTime = select.GetColumn(2); - item.IsRemoved = (select.GetColumn(5) != 0); - - if (!item.IsRemoved) - { - item.Manifest = select.GetColumn(3); - item.Hash = select.GetColumn(4); - } + item.Manifest = select.GetColumn(3); + item.Hash = select.GetColumn(4); result.emplace_back(std::move(item)); } @@ -292,12 +308,35 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return result; } + std::vector PackageUpdateTrackingTable::GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals) + { + std::vector result; + + if (removals == RemovalBehavior::Delete) + { + // Removals delete their row, so there is nothing to report. + return result; + } + + Builder::StatementBuilder builder; + builder.Select(s_PUTT_Package).From(s_PUTT_Table_Name). + Where(s_PUTT_WriteTime).IsGreaterThanOrEqualTo(updateBaseTime). + And(s_PUTT_IsRemoved).Equals(1); + + Statement select = builder.Prepare(connection); + + while (select.Step()) + { + result.emplace_back(select.GetColumn(0)); + } + + return result; + } + SQLite::blob_t PackageUpdateTrackingTable::GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier) { Builder::StatementBuilder builder; - builder.Select(s_PUTT_Hash).From(s_PUTT_Table_Name). - Where(s_PUTT_Package).LikeWithEscape(packageIdentifier). - And(s_PUTT_IsRemoved).Equals(0); + builder.Select(s_PUTT_Hash).From(s_PUTT_Table_Name).Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); Statement select = builder.Prepare(connection); @@ -306,25 +345,17 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return select.GetColumn(0); } - void PackageUpdateTrackingTable::EnsureIsRemovedColumn(SQLite::Connection& connection) + void PackageUpdateTrackingTable::AddIsRemovedColumn(SQLite::Connection& connection) { - // Use PRAGMA table_info to check whether is_removed already exists. - SQLite::Statement info = SQLite::Statement::Create(connection, "PRAGMA table_info(update_tracking)"); - bool hasColumn = false; - while (info.Step()) + // The table is created on demand, so an index that has never had a manifest written + // to it will not have one yet. It will be created with the column when it is needed. + if (!Exists(connection)) { - if (info.GetColumn(1) == std::string{ s_PUTT_IsRemoved }) - { - hasColumn = true; - break; - } + return; } - if (!hasColumn) - { - SQLite::Statement alter = SQLite::Statement::Create(connection, - "ALTER TABLE update_tracking ADD COLUMN is_removed INTEGER NOT NULL DEFAULT 0"); - alter.Execute(); - } + Builder::StatementBuilder builder; + builder.AlterTable(s_PUTT_Table_Name).Add(Builder::ColumnBuilder(s_PUTT_IsRemoved, Builder::Type::Int64).NotNull().Default(0)); + builder.Execute(connection); } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h index 6e4fedefae..d72b6eac54 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h @@ -11,14 +11,25 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // only the necessary package manifests. struct PackageUpdateTrackingTable { + // Determines how the removal of a package is reflected in the table. + // Schema 2.0 deletes the row outright, so the table has no record that the package + // ever existed. Schema 2.1 instead marks the row as removed, which is what allows a + // delta index to express a removal; that requires the `is_removed` column, which only + // exists on tables created or migrated by 2.1. + enum class RemovalBehavior + { + Delete, + Record, + }; + // Get the table name. static std::string_view TableName(); // Creates the table. - static void Create(SQLite::Connection& connection); + static void Create(SQLite::Connection& connection, RemovalBehavior removals); // Creates the table if it does not exist. - static void EnsureExists(SQLite::Connection& connection); + static void EnsureExists(SQLite::Connection& connection, RemovalBehavior removals); // Drops the table. static void Drop(SQLite::Connection& connection); @@ -27,11 +38,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 static bool Exists(const SQLite::Connection& connection); // Updates the tracking table for the given package identifier in the internal index. - static void Update(SQLite::Connection& connection, const ISQLiteIndex* internalIndex, const std::string& packageIdentifier, bool ensureTable = true); + static void Update(SQLite::Connection& connection, const ISQLiteIndex* internalIndex, const std::string& packageIdentifier, RemovalBehavior removals, bool ensureTable = true); // Checks the consistency of the index to ensure that every referenced row exists. // Returns true if index is consistent; false if it is not. - static bool CheckConsistency(const SQLite::Connection& connection, ISQLiteIndex* internalIndex, bool log); + static bool CheckConsistency(const SQLite::Connection& connection, ISQLiteIndex* internalIndex, RemovalBehavior removals, bool log); // Data on a single row in the table. struct PackageData @@ -41,18 +52,21 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 int64_t WriteTime = 0; SQLite::blob_t Manifest; SQLite::blob_t Hash; - bool IsRemoved = false; }; // Gets the data on updates that have been written since the given base time. - // Includes entries for removed packages (IsRemoved == true). - static std::vector GetUpdatesSince(const SQLite::Connection& connection, int64_t updateBaseTime); + // Removed packages are never included; use GetRemovalsSince to retrieve those. + static std::vector GetUpdatesSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); + + // Gets the identifiers of the packages removed since the given base time. + // Only meaningful when removals are being recorded; always empty otherwise. + static std::vector GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); // Gets the data hash for the given package identifier. static SQLite::blob_t GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier); - // Adds the is_removed column to the table if it does not already exist. - // Used when migrating from schema 2.0 to 2.1. - static void EnsureIsRemovedColumn(SQLite::Connection& connection); + // Adds the is_removed column to an existing table that does not have it. + // Used when migrating from schema 2.0 to 2.1; does nothing if the table does not exist. + static void AddIsRemovedColumn(SQLite::Connection& connection); }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h index cbd540c5a7..56258972ff 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h @@ -17,5 +17,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 // Version 2.0 bool MigrateFrom(SQLite::Connection& connection, const ISQLiteIndex* current) override; + + protected: + // Records removals in the update tracking table rather than deleting the row, + // so that delta generation can see which packages have gone away. + V2_0::PackageUpdateTrackingTable::RemovalBehavior GetTrackingRemovalBehavior() const override; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp index 799db58343..9a70f4dccb 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -19,16 +19,25 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 auto currentVersion = current->GetVersion(); + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migrate_from_v2_1"); + + // Attempt a migration to 2.0 first, which will only return true if it actually performed a migration + bool v2result = V2_0::Interface::MigrateFrom(connection, current); + // Migration from 2.0 → 2.1: add the is_removed column to update_tracking. - if (currentVersion.MajorVersion == 2 && currentVersion.MinorVersion == 0) + if (v2result || (currentVersion.MajorVersion == 2 && currentVersion.MinorVersion == 0)) { - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migrate_from_v2_1"); - V2_0::PackageUpdateTrackingTable::EnsureIsRemovedColumn(connection); + V2_0::PackageUpdateTrackingTable::AddIsRemovedColumn(connection); savepoint.Commit(); return true; } - // Fall through to V2_0 migration (handles 1.7 → 2.0 → 2.1 via two-step upgrade). - return V2_0::Interface::MigrateFrom(connection, current); + savepoint.Rollback(true); + return false; + } + + V2_0::PackageUpdateTrackingTable::RemovalBehavior Interface::GetTrackingRemovalBehavior() const + { + return V2_0::PackageUpdateTrackingTable::RemovalBehavior::Record; } } diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h index 58a90952a9..fede0c3311 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h @@ -437,7 +437,10 @@ namespace AppInstaller::SQLite::Builder StatementBuilder& AlterTable(std::initializer_list table); // Complete an alter table statement by adding a column. + // The SubBuilder form allows for constraints such as `not null` and a default value; + // SQLite requires a non-null default when adding a column declared as not null. StatementBuilder& Add(std::string_view column, Type type); + StatementBuilder& Add(const details::SubBuilder& column); // Begin a table deletion statement. // The initializer_list form enables the table name to be constructed from multiple parts. diff --git a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp index df1cf46f38..4363abe8a1 100644 --- a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp +++ b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp @@ -765,6 +765,12 @@ namespace AppInstaller::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::Add(const details::SubBuilder& column) + { + m_stream << " ADD " << column; + return *this; + } + StatementBuilder& StatementBuilder::DropTable(std::string_view table) { OutputOperationAndTable(m_stream, "DROP TABLE", table); From 94bbd9bee8dc796543b96dc6ee58960c6cf7b63a Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Wed, 2 Sep 2026 16:36:43 -0700 Subject: [PATCH 17/36] Add delta creation virt callout and package id stability test --- src/AppInstallerCLITests/SQLiteIndex.cpp | 66 +++++++++++++++++++ .../Microsoft/Schema/2_0/Interface.h | 6 ++ .../Microsoft/Schema/2_0/Interface_2_0.cpp | 7 ++ 3 files changed, 79 insertions(+) diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index f93b728d97..afc23c4b33 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -3967,6 +3967,72 @@ TEST_CASE("SQLiteIndex_VersionStringPreserved", "[sqliteindex]") REQUIRE(extractedVersion == version); } +namespace +{ + // Reads the rowid assigned to a package identifier in a prepared 2.0 index. + int64_t GetPreparedPackageRowId(const std::filesystem::path& indexPath, std::string_view packageIdentifier) + { + Connection connection = Connection::Create(indexPath.u8string(), Connection::OpenDisposition::ReadOnly); + Statement statement = Statement::Create(connection, "SELECT rowid FROM packages WHERE id = ?"); + statement.Bind(1, std::string{ packageIdentifier }); + REQUIRE(statement.Step()); + return statement.GetColumn(0); + } +} + +// The delta index relies on a package receiving the same packages rowid every time the same +// working index is prepared, even as other packages are added and removed around it. +TEST_CASE("SQLiteIndex_PrepareForPackaging_RowIdsAreStable", "[sqliteindex][V2_0]") +{ + TempFile workingFile{ "rowid_working"s, ".db"s }; + TempFile firstFile{ "rowid_first"s, ".db"s }; + TempFile secondFile{ "rowid_second"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + ManifestAndPath m3; + CreateFakeManifestAndPath(m3, "Publisher3", "1.0"); + ManifestAndPath m4; + CreateFakeManifestAndPath(m4, "Publisher4", "1.0"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 0 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + index.AddManifest(m3.Manifest, m3.Path); + } + + std::filesystem::copy_file(workingFile.GetPath(), firstFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + + { + SQLiteIndex prepared = SQLiteIndex::Open(firstFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + } + + // Remove the first package and add a new one, which would renumber the survivors without pinning. + { + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.RemoveManifest(m1.Manifest, m1.Path); + index.AddManifest(m4.Manifest, m4.Path); + } + + std::filesystem::copy_file(workingFile.GetPath(), secondFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + + { + SQLiteIndex prepared = SQLiteIndex::Open(secondFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + } + + REQUIRE(GetPreparedPackageRowId(firstFile.GetPath(), "Publisher2.Id") == GetPreparedPackageRowId(secondFile.GetPath(), "Publisher2.Id")); + REQUIRE(GetPreparedPackageRowId(firstFile.GetPath(), "Publisher3.Id") == GetPreparedPackageRowId(secondFile.GetPath(), "Publisher3.Id")); + + // The added package must not collide with any existing rowid. + REQUIRE(GetPreparedPackageRowId(secondFile.GetPath(), "Publisher4.Id") > GetPreparedPackageRowId(secondFile.GetPath(), "Publisher3.Id")); +} + TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_RemovalDeletesRow", "[sqliteindex][V2_0][updatetracking]") { TempFile indexFile{ "updatetracking"s, ".db"s }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h index 95a0136788..8f9289338f 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h @@ -86,6 +86,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Prepares for packaging, optionally vacuuming the database. virtual void PrepareForPackaging(const SQLiteIndexContext& context, bool vacuum); + // Extends PrepareForPackaging at the point where the 2.0 tables have been populated but + // the update tracking and internal 1.7 tables have not yet been dropped. That is the only + // window in which both the packaged and the pre-packaged forms of the data exist, so any + // output that must correlate the two has to be produced here. Does nothing by default. + virtual void CreateAdditionalPackagingOutput(const SQLiteIndexContext& context); + // Force the database to shrink the file size. // This *must* be done outside of an active transaction. void Vacuum(const SQLite::Connection& connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index 00a9e4befd..f185e57e1e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -739,6 +739,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } + void Interface::CreateAdditionalPackagingOutput(const SQLiteIndexContext&) + { + } + PackageUpdateTrackingTable::RemovalBehavior Interface::GetTrackingRemovalBehavior() const { return PackageUpdateTrackingTable::RemovalBehavior::Delete; @@ -1050,6 +1054,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } + // Extension point for later schema versions; see the declaration for why it must be here. + CreateAdditionalPackagingOutput(context); + // Generate the delta index before dropping the tracking table (which is needed for delta construction). // Delta generation is triggered by setting DeltaBaselineIndexPath and DeltaOutputPath on the context. if (context.Data.Contains(Property::DeltaBaselineIndexPath) && From 1da913718f03b545432b80bf06193565588da90e Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Thu, 3 Sep 2026 12:03:42 -0700 Subject: [PATCH 18/36] Delta generation move and productize --- src/AppInstallerCLITests/SQLiteIndex.cpp | 21 +- .../AppInstallerRepositoryCore.vcxproj | 4 + ...AppInstallerRepositoryCore.vcxproj.filters | 12 + .../Microsoft/Schema/2_0/Interface.h | 1 - .../Microsoft/Schema/2_0/Interface_2_0.cpp | 448 +----------------- .../Schema/2_0/OneToManyTableWithMap.cpp | 5 + .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 341 +++++++++++++ .../Microsoft/Schema/2_1/DeltaGeneration.h | 28 ++ .../Microsoft/Schema/2_1/DeltaTables.cpp | 160 +++++++ .../Microsoft/Schema/2_1/DeltaTables.h | 38 ++ .../Microsoft/Schema/2_1/Interface.h | 7 + .../Microsoft/Schema/2_1/Interface_2_1.cpp | 47 ++ 12 files changed, 665 insertions(+), 447 deletions(-) create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index afc23c4b33..9d93fe108b 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -4259,7 +4259,7 @@ TEST_CASE("SQLiteIndex_Delta_RemovedPackage", "[sqliteindex][V2_1][delta]") REQUIRE(idStmt.GetColumn(0) == "Publisher2.Id"); } -TEST_CASE("SQLiteIndex_Delta_NoChanges_NoDeltaFile", "[sqliteindex][V2_1][delta]") +TEST_CASE("SQLiteIndex_Delta_NoChanges_EmptyDelta", "[sqliteindex][V2_1][delta]") { TempFile workingFile{ "delta_working"s, ".db"s }; TempFile baselineFile{ "delta_baseline"s, ".db"s }; @@ -4289,8 +4289,23 @@ TEST_CASE("SQLiteIndex_Delta_NoChanges_NoDeltaFile", "[sqliteindex][V2_1][delta] index.PrepareForPackaging(); } - // No changes tracked after setting base time, so delta file should NOT have been created - REQUIRE(!std::filesystem::exists(deltaFile.GetPath())); + // A delta is produced even with nothing to report, so that a consumer never has to handle a + // missing delta as a special case. It is simply empty. + REQUIRE(std::filesystem::exists(deltaFile.GetPath())); + + Connection deltaConn = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + + for (std::string_view tableName : { + "delta_packages"sv, + "delta_pfns2"sv, "delta_productcodes2"sv, "delta_norm_names2"sv, "delta_norm_publishers2"sv, "delta_upgradecodes2"sv, + "delta_tags2"sv, "delta_tags2_map"sv, "delta_commands2"sv, "delta_commands2_map"sv }) + { + INFO(tableName); + + Statement countStmt = Statement::Create(deltaConn, "SELECT COUNT(*) FROM [" + std::string{ tableName } + "]"); + REQUIRE(countStmt.Step()); + REQUIRE(countStmt.GetColumn(0) == 0); + } } TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_Search", "[sqliteindex][V2_1][delta]") diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index 54fb4efa88..71172fabb9 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -331,6 +331,8 @@ + + @@ -444,6 +446,8 @@ + + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index 2aa553dbc9..25d88eb6d8 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -522,6 +522,12 @@ Header Files + + Microsoft\Schema\2_1 + + + Microsoft\Schema\2_1 + Microsoft\Schema\2_1 @@ -818,6 +824,12 @@ Source Files + + Microsoft\Schema\2_1 + + + Microsoft\Schema\2_1 + Microsoft\Schema\2_1 diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h index 8f9289338f..0372a838a5 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h @@ -15,7 +15,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { // Version 2.0 static constexpr std::string_view s_MetadataValueName_PackageUpdateTrackingBaseTime = "updateTrackingBase"sv; - static constexpr std::string_view s_MetadataValueName_DeltaBaselineTime = "deltaBaselineTime"sv; // Interface to this schema version exposed through ISQLiteIndex. struct Interface : public ISQLiteIndex diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index f185e57e1e..47195d407d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -80,309 +80,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } - namespace anon - { - // Executes a raw SQL statement on a connection using the statement builder mechanism. - void ExecuteSQL(SQLite::Connection& connection, std::string_view sql) - { - SQLite::Statement stmt = SQLite::Statement::Create(connection, sql); - stmt.Execute(); - } - - // Creates all delta tables in the delta connection. - void CreateDeltaSchema(SQLite::Connection& deltaConn) - { - ExecuteSQL(deltaConn, R"( - CREATE TABLE IF NOT EXISTS delta_packages ( - rowid INTEGER PRIMARY KEY, - id TEXT NOT NULL, - name TEXT NOT NULL, - moniker TEXT, - latest_version TEXT NOT NULL, - arp_min_version TEXT, - arp_max_version TEXT, - hash BLOB, - is_removed INTEGER NOT NULL DEFAULT 0 - ) - )"); - - // SystemReference string tables (value + package_id, no separate id) - static constexpr std::pair s_SysRefTables[] = { - { "pfns2", "pfn" }, - { "productcodes2", "productcode" }, - { "norm_names2", "norm_name" }, - { "norm_publishers2", "norm_publisher" }, - { "upgradecodes2", "upgradecode" }, - }; - for (const auto& [table, value] : s_SysRefTables) - { - std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + - " (" + std::string(value) + " TEXT NOT NULL, package INTEGER NOT NULL, " + - "is_removed INTEGER NOT NULL DEFAULT 0, " + - "PRIMARY KEY (" + std::string(value) + ", package)) WITHOUT ROWID"; - ExecuteSQL(deltaConn, sql); - } - - // OneToMany data tables (rowid + value) - static constexpr std::pair s_OneToManyTables[] = { - { "tags2", "tag" }, - { "commands2", "command" }, - }; - for (const auto& [table, value] : s_OneToManyTables) - { - std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + - " (rowid INTEGER PRIMARY KEY, " + std::string(value) + " TEXT NOT NULL)"; - ExecuteSQL(deltaConn, sql); - } - - // OneToMany map tables (value_rowid + package_rowid) - for (const auto& [table, value] : s_OneToManyTables) - { - std::string sql = "CREATE TABLE IF NOT EXISTS delta_" + std::string(table) + "_map" + - " (" + std::string(value) + " INTEGER NOT NULL, package INTEGER NOT NULL, " + - "is_removed INTEGER NOT NULL DEFAULT 0, " + - "PRIMARY KEY (" + std::string(value) + ", package)) WITHOUT ROWID"; - ExecuteSQL(deltaConn, sql); - } - } - - // Returns the rowid of a package in the baseline, or 0 if not found. - SQLite::rowid_t GetBaselinePackageRowid(SQLite::Connection& baselineConn, const std::string& packageId) - { - SQLite::Builder::StatementBuilder builder; - builder.Select(SQLite::RowIDName).From("packages").Where("id").Equals(packageId); - SQLite::Statement stmt = builder.Prepare(baselineConn); - if (stmt.Step()) - { - return stmt.GetColumn(0); - } - return 0; - } - - // Returns the max rowid in the packages table, or 0 if empty. - SQLite::rowid_t GetMaxPackageRowid(SQLite::Connection& baselineConn) - { - SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, "SELECT MAX(rowid) FROM packages"); - if (stmt.Step()) - { - // MAX(rowid) returns NULL if table is empty - if (!stmt.GetColumnIsNull(0)) - { - return stmt.GetColumn(0); - } - } - return 0; - } - - // Returns the max rowid in a data table (tags2 or commands2), or 0 if empty. - SQLite::rowid_t GetMaxDataTableRowid(SQLite::Connection& baselineConn, std::string_view tableName) - { - std::string sql = "SELECT MAX(rowid) FROM " + std::string(tableName); - SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); - if (stmt.Step() && !stmt.GetColumnIsNull(0)) - { - return stmt.GetColumn(0); - } - return 0; - } - - // Returns the rowid in baseline data table for the given value string, or 0 if not present. - SQLite::rowid_t GetBaselineDataTableRowid(SQLite::Connection& baselineConn, std::string_view tableName, std::string_view valueName, const std::string& value) - { - std::string sql = "SELECT rowid FROM " + std::string(tableName) + " WHERE " + std::string(valueName) + " = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); - stmt.Bind(1, value); - if (stmt.Step()) - { - return stmt.GetColumn(0); - } - return 0; - } - - // Inserts or finds a value in delta data table; returns the rowid (possibly from baseline). - // baselineMaxRowid: the starting offset for new delta rowids. - SQLite::rowid_t EnsureDeltaDataTableValue( - SQLite::Connection& deltaConn, - SQLite::Connection& baselineConn, - std::string_view deltaTableName, - std::string_view valueName, - const std::string& value, - SQLite::rowid_t& nextNewRowid) - { - // Check if the value is already in the baseline - SQLite::rowid_t baselineRowid = GetBaselineDataTableRowid(baselineConn, std::string(deltaTableName).substr(6), valueName, value); - if (baselineRowid != 0) - { - return baselineRowid; - } - - // Check if already in the delta table - std::string selectSql = "SELECT rowid FROM " + std::string(deltaTableName) + " WHERE " + std::string(valueName) + " = ?"; - SQLite::Statement selectStmt = SQLite::Statement::Create(deltaConn, selectSql); - selectStmt.Bind(1, value); - if (selectStmt.Step()) - { - return selectStmt.GetColumn(0); - } - - // Insert as a new entry - SQLite::rowid_t newRowid = ++nextNewRowid; - std::string insertSql = "INSERT INTO " + std::string(deltaTableName) + " (rowid, " + std::string(valueName) + ") VALUES (?, ?)"; - SQLite::Statement insertStmt = SQLite::Statement::Create(deltaConn, insertSql); - insertStmt.Bind(1, newRowid); - insertStmt.Bind(2, value); - insertStmt.Execute(); - return newRowid; - } - - // Processes a SystemReference table for a changed package. - // Compares current values vs baseline values and records adds/removes. - void ProcessDeltaSysRefTable( - SQLite::Connection& deltaConn, - SQLite::Connection& sourceConn, - SQLite::Connection& baselineConn, - std::string_view tableName, - std::string_view valueName, - SQLite::rowid_t packageRowid, - const std::string& packageId) - { - UNREFERENCED_PARAMETER(packageId); - std::string deltaTable = "delta_" + std::string(tableName); - std::string primaryCol = "package"; - - // Get current values from the new V2 index - std::vector currentValues; - { - std::string sql = "SELECT " + std::string(valueName) + " FROM " + std::string(tableName) + " WHERE " + primaryCol + " = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(sourceConn, sql); - stmt.Bind(1, packageRowid); - while (stmt.Step()) - { - currentValues.push_back(stmt.GetColumn(0)); - } - } - - // Get baseline values - std::vector baselineValues; - { - std::string sql = "SELECT " + std::string(valueName) + " FROM " + std::string(tableName) + " WHERE " + primaryCol + " = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); - stmt.Bind(1, packageRowid); - while (stmt.Step()) - { - baselineValues.push_back(stmt.GetColumn(0)); - } - } - - // Find added values (in current but not baseline) - for (const auto& val : currentValues) - { - if (std::find(baselineValues.begin(), baselineValues.end(), val) == baselineValues.end()) - { - std::string sql = "INSERT OR IGNORE INTO " + deltaTable + - " (" + std::string(valueName) + ", " + primaryCol + ", is_removed) VALUES (?, ?, 0)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, val); - stmt.Bind(2, packageRowid); - stmt.Execute(); - } - } - - // Find removed values (in baseline but not current) - for (const auto& val : baselineValues) - { - if (std::find(currentValues.begin(), currentValues.end(), val) == currentValues.end()) - { - std::string sql = "INSERT OR IGNORE INTO " + deltaTable + - " (" + std::string(valueName) + ", " + primaryCol + ", is_removed) VALUES (?, ?, 1)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, val); - stmt.Bind(2, packageRowid); - stmt.Execute(); - } - } - } - - // Processes a OneToMany table for a changed package. - void ProcessDeltaOneToManyTable( - SQLite::Connection& deltaConn, - SQLite::Connection& sourceConn, - SQLite::Connection& baselineConn, - std::string_view tableName, - std::string_view valueName, - SQLite::rowid_t packageRowid, - SQLite::rowid_t& nextNewDataRowid) - { - std::string deltaDataTable = "delta_" + std::string(tableName); - std::string deltaMapTable = "delta_" + std::string(tableName) + "_map"; - std::string mapTable = std::string(tableName) + "_map"; - - // Get current values via join (tags2_map JOIN tags2) - std::vector currentValues; - { - std::string sql = "SELECT t." + std::string(valueName) + - " FROM " + mapTable + " m JOIN " + std::string(tableName) + " t ON m." + std::string(valueName) + " = t.rowid" + - " WHERE m.package = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(sourceConn, sql); - stmt.Bind(1, packageRowid); - while (stmt.Step()) - { - currentValues.push_back(stmt.GetColumn(0)); - } - } - - // Get baseline values via join - std::vector baselineValues; - { - std::string sql = "SELECT t." + std::string(valueName) + - " FROM " + mapTable + " m JOIN " + std::string(tableName) + " t ON m." + std::string(valueName) + " = t.rowid" + - " WHERE m.package = ?"; - SQLite::Statement stmt = SQLite::Statement::Create(baselineConn, sql); - stmt.Bind(1, packageRowid); - while (stmt.Step()) - { - baselineValues.push_back(stmt.GetColumn(0)); - } - } - - // Record added mappings (current but not baseline) - for (const auto& val : currentValues) - { - if (std::find(baselineValues.begin(), baselineValues.end(), val) == baselineValues.end()) - { - SQLite::rowid_t dataRowid = EnsureDeltaDataTableValue( - deltaConn, baselineConn, deltaDataTable, valueName, val, nextNewDataRowid); - - std::string sql = "INSERT OR IGNORE INTO " + deltaMapTable + - " (" + std::string(valueName) + ", package, is_removed) VALUES (?, ?, 0)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, dataRowid); - stmt.Bind(2, packageRowid); - stmt.Execute(); - } - } - - // Record removed mappings (baseline but not current) - for (const auto& val : baselineValues) - { - if (std::find(currentValues.begin(), currentValues.end(), val) == currentValues.end()) - { - // Find the rowid — it's in the baseline data table - SQLite::rowid_t dataRowid = GetBaselineDataTableRowid(baselineConn, std::string(tableName), valueName, val); - if (dataRowid != 0) - { - std::string sql = "INSERT OR IGNORE INTO " + deltaMapTable + - " (" + std::string(valueName) + ", package, is_removed) VALUES (?, ?, 1)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, dataRowid); - stmt.Bind(2, packageRowid); - stmt.Execute(); - } - } - } - } - } - Interface::Interface(Utility::NormalizationVersion normVersion) : m_normalizer(normVersion) { } @@ -934,11 +631,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { SQLite::Connection& connection = context.Connection; - // TODO: We may need to set the baseline time to the max update tracking time +1 to only catch new incoming changes - // This assumes some delay between delta generation and the next package update. - // TODO: We also need to ensure that our times are UTC / not impacted by timezone shifts, etc. - SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_DeltaBaselineTime, std::to_string(Utility::GetCurrentUnixEpoch())); - // Get the base time from metadata int64_t updateBaseTime = 0; std::optional updateBaseTimeString = SQLite::MetadataTable::TryGetNamedValue(connection, s_MetadataValueName_PackageUpdateTrackingBaseTime); @@ -1057,136 +749,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Extension point for later schema versions; see the declaration for why it must be here. CreateAdditionalPackagingOutput(context); - // Generate the delta index before dropping the tracking table (which is needed for delta construction). - // Delta generation is triggered by setting DeltaBaselineIndexPath and DeltaOutputPath on the context. - if (context.Data.Contains(Property::DeltaBaselineIndexPath) && - context.Data.Contains(Property::DeltaOutputPath)) - { - // Delta packaging requires schema 2.1+ (is_removed column in update_tracking). - THROW_WIN32_IF(ERROR_NOT_SUPPORTED, GetVersion().MinorVersion < 1); - - std::filesystem::path baselinePath = context.Data.Get(); - std::filesystem::path deltaOutputPath = context.Data.Get(); - - AICLI_LOG(Repo, Info, << "Generating delta index at [" << deltaOutputPath << "] against baseline [" << baselinePath << "]"); - - SQLite::Connection baselineConn = SQLite::Connection::Create(baselinePath.u8string(), SQLite::Connection::OpenDisposition::ReadOnly); - - int64_t deltaUpdateBaseTime = 0; - std::optional deltaUpdateBaseTimeString = SQLite::MetadataTable::TryGetNamedValue(baselineConn, s_MetadataValueName_DeltaBaselineTime); - if (deltaUpdateBaseTimeString && !deltaUpdateBaseTimeString->empty()) - { - deltaUpdateBaseTime = std::stoll(deltaUpdateBaseTimeString.value()); - } - - auto changedPackages = PackageUpdateTrackingTable::GetUpdatesSince(connection, deltaUpdateBaseTime, GetTrackingRemovalBehavior()); - auto removedPackages = PackageUpdateTrackingTable::GetRemovalsSince(connection, deltaUpdateBaseTime, GetTrackingRemovalBehavior()); - - SQLite::Connection deltaConn = SQLite::Connection::Create( - deltaOutputPath.u8string(), SQLite::Connection::OpenDisposition::Create); - - anon::CreateDeltaSchema(deltaConn); - - SQLite::rowid_t maxBaselinePackageRowid = anon::GetMaxPackageRowid(baselineConn); - SQLite::rowid_t nextNewPackageRowid = maxBaselinePackageRowid; - - SQLite::rowid_t maxBaselineTagsRowid = anon::GetMaxDataTableRowid(baselineConn, "tags2"); - SQLite::rowid_t nextNewTagsRowid = maxBaselineTagsRowid; - - SQLite::rowid_t maxBaselineCommandsRowid = anon::GetMaxDataTableRowid(baselineConn, "commands2"); - SQLite::rowid_t nextNewCommandsRowid = maxBaselineCommandsRowid; - - SQLite::Savepoint deltaSavepoint = SQLite::Savepoint::Create(deltaConn, "delta_build"); - - for (const auto& packageIdentifier : removedPackages) - { - SQLite::rowid_t packageRowid = anon::GetBaselinePackageRowid(baselineConn, packageIdentifier); - - if (packageRowid == 0) - { - // Package was added and removed within the same tracking window; skip. - continue; - } - - AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << packageIdentifier << "] (rowid=" << packageRowid << ")"); - - std::string sql = "INSERT OR REPLACE INTO delta_packages (rowid, id, name, latest_version, is_removed) VALUES (?, ?, '', '', 1)"; - SQLite::Statement stmt = SQLite::Statement::Create(deltaConn, sql); - stmt.Bind(1, packageRowid); - stmt.Bind(2, packageIdentifier); - stmt.Execute(); - } - - for (const auto& pkg : changedPackages) - { - SQLite::rowid_t packageRowid = anon::GetBaselinePackageRowid(baselineConn, pkg.PackageIdentifier); - - { - bool isNewPackage = (packageRowid == 0); - if (isNewPackage) - { - packageRowid = ++nextNewPackageRowid; - } - - AICLI_LOG(Repo, Verbose, << "Delta: recording " << (isNewPackage ? "addition" : "update") << " of [" << pkg.PackageIdentifier << "] (rowid=" << packageRowid << ")"); - - { - std::string sql = "SELECT id, name, moniker, latest_version, arp_min_version, arp_max_version, hash " - "FROM packages WHERE id LIKE ?"; - SQLite::Statement stmt = SQLite::Statement::Create(connection, sql); - stmt.Bind(1, pkg.PackageIdentifier); - THROW_HR_IF(E_NOT_SET, !stmt.Step()); - - std::string id = stmt.GetColumn(0); - std::string name = stmt.GetColumn(1); - std::string moniker = stmt.GetColumnIsNull(2) ? "" : stmt.GetColumn(2); - std::string latestVersion = stmt.GetColumn(3); - std::string arpMin = stmt.GetColumnIsNull(4) ? "" : stmt.GetColumn(4); - std::string arpMax = stmt.GetColumnIsNull(5) ? "" : stmt.GetColumn(5); - SQLite::blob_t hash = stmt.GetColumnIsNull(6) ? SQLite::blob_t{} : stmt.GetColumn(6); - - std::string insertSql = - "INSERT OR REPLACE INTO delta_packages " - "(rowid, id, name, moniker, latest_version, arp_min_version, arp_max_version, hash, is_removed) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)"; - SQLite::Statement insertStmt = SQLite::Statement::Create(deltaConn, insertSql); - insertStmt.Bind(1, packageRowid); - insertStmt.Bind(2, id); - insertStmt.Bind(3, name); - if (moniker.empty()) insertStmt.Bind(4, nullptr); else insertStmt.Bind(4, moniker); - insertStmt.Bind(5, latestVersion); - if (arpMin.empty()) insertStmt.Bind(6, nullptr); else insertStmt.Bind(6, arpMin); - if (arpMax.empty()) insertStmt.Bind(7, nullptr); else insertStmt.Bind(7, arpMax); - if (hash.empty()) insertStmt.Bind(8, nullptr); else insertStmt.Bind(8, hash); - insertStmt.Execute(); - } - - static constexpr std::pair s_DeltaSysRefTables[] = { - { "pfns2", "pfn" }, - { "productcodes2", "productcode" }, - { "norm_names2", "norm_name" }, - { "norm_publishers2", "norm_publisher" }, - { "upgradecodes2", "upgradecode" }, - }; - - for (const auto& [table, value] : s_DeltaSysRefTables) - { - anon::ProcessDeltaSysRefTable(deltaConn, connection, baselineConn, - table, value, packageRowid, pkg.PackageIdentifier); - } - - anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, - "tags2", "tag", packageRowid, nextNewTagsRowid); - anon::ProcessDeltaOneToManyTable(deltaConn, connection, baselineConn, - "commands2", "command", packageRowid, nextNewCommandsRowid); - } - } - - deltaSavepoint.Commit(); - - AICLI_LOG(Repo, Info, << "Delta index generation complete"); - } - PackagesTable::PrepareForPackaging< PackagesTable::IdColumn, PackagesTable::NameColumn, @@ -1259,7 +821,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // TEMP VIEW: packages // Delta entries (added/updated) override baseline; removed packages are excluded. - anon::ExecuteSQL(connection, R"( + SQLite::Statement::Create(connection, R"( CREATE TEMP VIEW packages AS SELECT rowid, id, name, moniker, latest_version, arp_min_version, arp_max_version, hash FROM delta_packages WHERE is_removed = 0 @@ -1267,7 +829,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 SELECT p.rowid, p.id, p.name, p.moniker, p.latest_version, p.arp_min_version, p.arp_max_version, p.hash FROM baseline.packages p WHERE p.id NOT IN (SELECT id FROM delta_packages) - )"); + )").Execute(); // TEMP VIEWs: SystemReference tables (pfns2, productcodes2, norm_names2, norm_publishers2, upgradecodes2) // For changed packages: delta has the full current set (is_removed=0 = current, is_removed=1 = removed). @@ -1287,7 +849,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 "UNION ALL " "SELECT " + std::string(value) + ", package FROM baseline." + std::string(table) + " " "WHERE package NOT IN (SELECT package FROM delta_" + std::string(table) + ")"; - anon::ExecuteSQL(connection, sql); + SQLite::Statement::Create(connection, sql).Execute(); } // TEMP VIEW: tags2 / commands2 data tables (rowid + value). @@ -1303,7 +865,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 "SELECT rowid, " + std::string(value) + " FROM delta_" + std::string(table) + " " "UNION ALL " "SELECT rowid, " + std::string(value) + " FROM baseline." + std::string(table); - anon::ExecuteSQL(connection, sql); + SQLite::Statement::Create(connection, sql).Execute(); // TEMP VIEW: tags2_map / commands2_map // For changed packages: delta has the full current set of mappings. @@ -1315,7 +877,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 "SELECT bm." + std::string(value) + ", bm.package " "FROM baseline." + std::string(table) + "_map bm " "WHERE bm.package NOT IN (SELECT package FROM delta_" + std::string(table) + "_map)"; - anon::ExecuteSQL(connection, mapSql); + SQLite::Statement::Create(connection, mapSql).Execute(); } m_isDeltaReadMode = true; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp index 76f650db82..a1ec81e301 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp @@ -18,6 +18,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 static constexpr std::string_view s_OneToManyTableWithMap_MapTable_IndexSuffix = "_index"sv; static constexpr std::string_view s_OneToManyTableWithMap_PrimaryKeyIndexSuffix = "_pkindex"sv; + std::string_view OneToManyTableGetManifestColumnName() + { + return s_OneToManyTableWithMap_MapTable_PrimaryName; + } + namespace anon { // Create the mapping table insert statement for multiple use. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp new file mode 100644 index 0000000000..f6720455c7 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/Schema/2_1/DeltaGeneration.h" +#include "Microsoft/Schema/2_1/DeltaTables.h" + +#include "Microsoft/Schema/2_0/PackagesTable.h" +#include "Microsoft/Schema/2_0/OneToManyTableWithMap.h" +#include "Microsoft/Schema/2_0/SystemReferenceStringTable.h" + +#include + +#include +#include +#include + + +namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta +{ + using namespace SQLite::Builder; + + namespace + { + // Gets the rowid of the given package identifier in the packages table, if it is present. + std::optional SelectPackageRowId(const SQLite::Connection& connection, const std::string& packageIdentifier) + { + StatementBuilder builder; + builder.Select(SQLite::RowIDName).From(V2_0::PackagesTable::TableName()). + Where(V2_0::PackagesTable::IdColumn::Name).Equals(packageIdentifier); + + SQLite::Statement statement = builder.Prepare(connection); + + if (statement.Step()) + { + return statement.GetColumn(0); + } + + return {}; + } + + // Gets the rowid of the given value in a one to many data table, if it is present. + std::optional SelectValueRowId( + const SQLite::Connection& connection, + std::string_view tableName, + std::string_view valueName, + const std::string& value) + { + StatementBuilder builder; + builder.Select(SQLite::RowIDName).From(tableName).Where(valueName).Equals(value); + + SQLite::Statement statement = builder.Prepare(connection); + + if (statement.Step()) + { + return statement.GetColumn(0); + } + + return {}; + } + + // Gets the largest rowid in the given table, or 0 when it is empty. + SQLite::rowid_t GetMaximumRowId(const SQLite::Connection& connection, std::string_view tableName) + { + StatementBuilder builder; + builder.Select().Column(Aggregate::Max, SQLite::RowIDName).From(tableName); + + SQLite::Statement statement = builder.Prepare(connection); + + // The aggregate produces a single row holding null when the table is empty. + if (statement.Step() && !statement.GetColumnIsNull(0)) + { + return statement.GetColumn(0); + } + + return 0; + } + + // Determines the values that are in the first collection but not the second. + std::vector GetValuesNotIn(const std::vector& values, const std::vector& other) + { + std::vector result; + + for (const std::string& value : values) + { + if (std::find(other.begin(), other.end(), value) == other.end()) + { + result.emplace_back(value); + } + } + + return result; + } + + // Records that the package with the given identifier is no longer present. + // The rowid is the one that the baseline gave the package, as that is what the rest of the + // baseline data refers to. + void WriteRemovedPackage(SQLite::Connection& deltaConnection, SQLite::rowid_t packageRowId, const std::string& packageIdentifier) + { + std::string tableName = GetTableName(V2_0::PackagesTable::TableName()); + + StatementBuilder builder; + builder.InsertInto(tableName). + Columns({ SQLite::RowIDName, V2_0::PackagesTable::IdColumn::Name, IsRemovedColumnName() }). + Values(packageRowId, packageIdentifier, 1); + + builder.Execute(deltaConnection); + } + + // Copies the current state of a package into the delta. + void WriteChangedPackage(SQLite::Connection& deltaConnection, const SQLite::Connection& sourceConnection, SQLite::rowid_t packageRowId) + { + auto [id, name, moniker, latestVersion, arpMinVersion, arpMaxVersion, hash] = + V2_0::PackagesTable::GetValuesById< + V2_0::PackagesTable::IdColumn, + V2_0::PackagesTable::NameColumn, + V2_0::PackagesTable::MonikerColumn, + V2_0::PackagesTable::LatestVersionColumn, + V2_0::PackagesTable::ARPMinVersionColumn, + V2_0::PackagesTable::ARPMaxVersionColumn, + V2_0::PackagesTable::HashColumn + >(sourceConnection, packageRowId); + + std::string tableName = GetTableName(V2_0::PackagesTable::TableName()); + + StatementBuilder builder; + builder.InsertInto(tableName). + Columns({ + SQLite::RowIDName, + V2_0::PackagesTable::IdColumn::Name, + V2_0::PackagesTable::NameColumn::Name, + V2_0::PackagesTable::MonikerColumn::Name, + V2_0::PackagesTable::LatestVersionColumn::Name, + V2_0::PackagesTable::ARPMinVersionColumn::Name, + V2_0::PackagesTable::ARPMaxVersionColumn::Name, + V2_0::PackagesTable::HashColumn::Name, + IsRemovedColumnName() }). + Values(packageRowId, id, name, moniker, latestVersion, arpMinVersion, arpMaxVersion, hash, 0); + + builder.Execute(deltaConnection); + } + + // Records a single system reference value as added or removed for a package. + void WriteSystemReferenceValue( + SQLite::Connection& deltaConnection, + const ValueTableInfo& table, + const std::string& value, + SQLite::rowid_t packageRowId, + bool isRemoved) + { + std::string tableName = GetTableName(table.TableName); + + StatementBuilder builder; + builder.InsertInto(tableName). + Columns({ table.ValueName, V2_0::details::SystemReferenceStringTableGetPrimaryColumnName(), IsRemovedColumnName() }). + Values(value, packageRowId, isRemoved ? 1 : 0); + + builder.Execute(deltaConnection); + } + + // Records only the system reference values that changed for the package, rather than its + // entire set of values. A package with many product codes that gains one more therefore + // costs a single row. + void WriteSystemReferenceDifference( + SQLite::Connection& deltaConnection, + const SQLite::Connection& sourceConnection, + const SQLite::Connection& baselineConnection, + const ValueTableInfo& table, + SQLite::rowid_t packageRowId) + { + std::vector currentValues = V2_0::details::SystemReferenceStringTableGetValuesByPrimaryId( + sourceConnection, table.TableName, table.ValueName, packageRowId); + std::vector baselineValues = V2_0::details::SystemReferenceStringTableGetValuesByPrimaryId( + baselineConnection, table.TableName, table.ValueName, packageRowId); + + for (const std::string& value : GetValuesNotIn(currentValues, baselineValues)) + { + WriteSystemReferenceValue(deltaConnection, table, value, packageRowId, false); + } + + for (const std::string& value : GetValuesNotIn(baselineValues, currentValues)) + { + WriteSystemReferenceValue(deltaConnection, table, value, packageRowId, true); + } + } + + // Gets a rowid that identifies the value in the merged data table, creating one if needed. + // Reusing the baseline's rowid where possible keeps the delta data table to just the values + // that the baseline has never seen; new rowids continue above the baseline's maximum so + // that the two tables can be combined without renumbering either of them. + SQLite::rowid_t EnsureValueRowId( + SQLite::Connection& deltaConnection, + const SQLite::Connection& baselineConnection, + const ValueTableInfo& table, + const std::string& value, + SQLite::rowid_t& nextValueRowId) + { + std::optional baselineRowId = SelectValueRowId(baselineConnection, table.TableName, table.ValueName, value); + + if (baselineRowId) + { + return baselineRowId.value(); + } + + std::string tableName = GetTableName(table.TableName); + + std::optional deltaRowId = SelectValueRowId(deltaConnection, tableName, table.ValueName, value); + + if (deltaRowId) + { + return deltaRowId.value(); + } + + SQLite::rowid_t newRowId = ++nextValueRowId; + + StatementBuilder builder; + builder.InsertInto(tableName). + Columns({ SQLite::RowIDName, table.ValueName }). + Values(newRowId, value); + + builder.Execute(deltaConnection); + + return newRowId; + } + + // Records a single map entry as added or removed for a package. + void WriteOneToManyValue( + SQLite::Connection& deltaConnection, + const ValueTableInfo& table, + SQLite::rowid_t valueRowId, + SQLite::rowid_t packageRowId, + bool isRemoved) + { + std::string mapTableName = GetMapTableName(table.TableName); + + StatementBuilder builder; + builder.InsertInto(mapTableName). + Columns({ table.ValueName, V2_0::details::OneToManyTableGetManifestColumnName(), IsRemovedColumnName() }). + Values(valueRowId, packageRowId, isRemoved ? 1 : 0); + + builder.Execute(deltaConnection); + } + + // Records only the map entries that changed for the package; see the note on the system + // reference equivalent for why the full set is not written. + void WriteOneToManyDifference( + SQLite::Connection& deltaConnection, + const SQLite::Connection& sourceConnection, + const SQLite::Connection& baselineConnection, + const ValueTableInfo& table, + SQLite::rowid_t packageRowId, + SQLite::rowid_t& nextValueRowId) + { + std::vector currentValues = V2_0::details::OneToManyTableWithMapGetValuesByPrimaryId( + sourceConnection, table.TableName, table.ValueName, packageRowId); + std::vector baselineValues = V2_0::details::OneToManyTableWithMapGetValuesByPrimaryId( + baselineConnection, table.TableName, table.ValueName, packageRowId); + + for (const std::string& value : GetValuesNotIn(currentValues, baselineValues)) + { + SQLite::rowid_t valueRowId = EnsureValueRowId(deltaConnection, baselineConnection, table, value, nextValueRowId); + WriteOneToManyValue(deltaConnection, table, valueRowId, packageRowId, false); + } + + for (const std::string& value : GetValuesNotIn(baselineValues, currentValues)) + { + // The value came from the baseline's own data table, so it must be found there. + std::optional valueRowId = SelectValueRowId(baselineConnection, table.TableName, table.ValueName, value); + THROW_HR_IF(E_NOT_VALID_STATE, !valueRowId); + + WriteOneToManyValue(deltaConnection, table, valueRowId.value(), packageRowId, true); + } + } + } + + void Generate( + const SQLite::Connection& sourceConnection, + const SQLite::Connection& baselineConnection, + const std::filesystem::path& deltaOutputPath, + const std::vector& changedPackages, + const std::vector& removedPackages) + { + AICLI_LOG(Repo, Info, << "Generating delta index at [" << deltaOutputPath << "] for " << changedPackages.size() << + " changed and " << removedPackages.size() << " removed packages"); + + SQLite::Connection deltaConnection = SQLite::Connection::Create(deltaOutputPath.u8string(), SQLite::Connection::OpenDisposition::Create); + + CreateTables(deltaConnection); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(deltaConnection, "delta_generate_v2_1"); + + std::map nextValueRowIds; + + for (const auto& table : OneToManyTables()) + { + nextValueRowIds[table.TableName] = GetMaximumRowId(baselineConnection, table.TableName); + } + + for (const std::string& packageIdentifier : removedPackages) + { + std::optional packageRowId = SelectPackageRowId(baselineConnection, packageIdentifier); + + if (!packageRowId) + { + // The package was both added and removed since the baseline was produced, so as far + // as the baseline is concerned it never existed and there is nothing to suppress. + AICLI_LOG(Repo, Verbose, << "Delta: [" << packageIdentifier << "] was removed but is not in the baseline"); + continue; + } + + AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << packageIdentifier << "] (rowid " << packageRowId.value() << ")"); + + WriteRemovedPackage(deltaConnection, packageRowId.value(), packageIdentifier); + } + + for (const auto& package : changedPackages) + { + // The rowid comes from the source rather than the baseline so that packages new to this + // delta are covered by the same lookup; rowid stability is what makes the two agree. + std::optional packageRowId = SelectPackageRowId(sourceConnection, package.PackageIdentifier); + THROW_HR_IF(E_NOT_VALID_STATE, !packageRowId); + + AICLI_LOG(Repo, Verbose, << "Delta: recording change to [" << package.PackageIdentifier << "] (rowid " << packageRowId.value() << ")"); + + WriteChangedPackage(deltaConnection, sourceConnection, packageRowId.value()); + + for (const auto& table : SystemReferenceTables()) + { + WriteSystemReferenceDifference(deltaConnection, sourceConnection, baselineConnection, table, packageRowId.value()); + } + + for (const auto& table : OneToManyTables()) + { + WriteOneToManyDifference(deltaConnection, sourceConnection, baselineConnection, table, packageRowId.value(), nextValueRowIds[table.TableName]); + } + } + + savepoint.Commit(); + + AICLI_LOG(Repo, Info, << "Delta index generation complete"); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h new file mode 100644 index 0000000000..8102a9aee2 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/2_0/PackageUpdateTrackingTable.h" +#include +#include +#include +#include + + +namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta +{ + // Writes a delta database describing the difference between a baseline index and the index + // that is currently being packaged. + // + // This must run while the index being packaged still holds its update tracking table, as that + // is the only record of which packages have changed since the baseline was produced. + // + // The source and the baseline assign the same rowid to a given package, so a package that + // exists in both is described by rows that carry its baseline rowid, and a package that is new + // to the source carries a rowid that the baseline cannot have used. + void Generate( + const SQLite::Connection& sourceConnection, + const SQLite::Connection& baselineConnection, + const std::filesystem::path& deltaOutputPath, + const std::vector& changedPackages, + const std::vector& removedPackages); +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp new file mode 100644 index 0000000000..ed3ba42fa8 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/Schema/2_1/DeltaTables.h" + +#include "Microsoft/Schema/2_0/PackagesTable.h" +#include "Microsoft/Schema/2_0/TagsTable.h" +#include "Microsoft/Schema/2_0/CommandsTable.h" +#include "Microsoft/Schema/2_0/PackageFamilyNameTable.h" +#include "Microsoft/Schema/2_0/ProductCodeTable.h" +#include "Microsoft/Schema/2_0/NormalizedPackageNameTable.h" +#include "Microsoft/Schema/2_0/NormalizedPackagePublisherTable.h" +#include "Microsoft/Schema/2_0/UpgradeCodeTable.h" + +#include + + +namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta +{ + using namespace std::string_view_literals; + + namespace + { + constexpr std::string_view s_Delta_TablePrefix = "delta_"sv; + constexpr std::string_view s_Delta_MapTableSuffix = "_map"sv; + constexpr std::string_view s_Delta_ValueIndexSuffix = "_pkindex"sv; + constexpr std::string_view s_Delta_IsRemovedColumn = "is_removed"sv; + + template + ValueTableInfo MakeValueTableInfo() + { + return { Table::TableName(), Table::ValueName() }; + } + } + + std::vector SystemReferenceTables() + { + return std::vector{ + MakeValueTableInfo(), + MakeValueTableInfo(), + MakeValueTableInfo(), + MakeValueTableInfo(), + MakeValueTableInfo(), + }; + } + + std::vector OneToManyTables() + { + return std::vector{ + MakeValueTableInfo(), + MakeValueTableInfo(), + }; + } + + std::string GetTableName(std::string_view baseTableName) + { + auto result = std::string{ s_Delta_TablePrefix }; + result.append(baseTableName); + return result; + } + + std::string GetMapTableName(std::string_view baseTableName) + { + auto result = GetTableName(baseTableName); + result.append(s_Delta_MapTableSuffix); + return result; + } + + std::string_view IsRemovedColumnName() + { + return s_Delta_IsRemovedColumn; + } + + void CreateTables(SQLite::Connection& connection) + { + using namespace SQLite::Builder; + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "delta_createtables_v2_1"); + + // The packages table mirrors its 2.0 counterpart, with the addition of the removal flag. + // Every column other than the identifier is nullable here, because a row that records a + // removal carries no data beyond the identity of what was removed. + { + std::string tableName = GetTableName(V2_0::PackagesTable::TableName()); + + StatementBuilder builder; + builder.CreateTable(tableName).Columns({ + IntegerPrimaryKey(), + ColumnBuilder(V2_0::PackagesTable::IdColumn::Name, Type::Text).NotNull(), + ColumnBuilder(V2_0::PackagesTable::NameColumn::Name, Type::Text), + ColumnBuilder(V2_0::PackagesTable::MonikerColumn::Name, Type::Text), + ColumnBuilder(V2_0::PackagesTable::LatestVersionColumn::Name, Type::Text), + ColumnBuilder(V2_0::PackagesTable::ARPMinVersionColumn::Name, Type::Text), + ColumnBuilder(V2_0::PackagesTable::ARPMaxVersionColumn::Name, Type::Text), + ColumnBuilder(V2_0::PackagesTable::HashColumn::Name, Type::Blob), + ColumnBuilder(s_Delta_IsRemovedColumn, Type::Int64).NotNull() + }); + builder.Execute(connection); + + // The merged view excludes baseline packages by identifier, so that lookup must be fast. + StatementBuilder indexBuilder; + indexBuilder.CreateUniqueIndex({ tableName, s_Delta_ValueIndexSuffix }). + On(tableName).Columns(V2_0::PackagesTable::IdColumn::Name); + indexBuilder.Execute(connection); + } + + // The system reference tables hold the value itself, so the delta only adds the removal flag. + for (const auto& table : SystemReferenceTables()) + { + std::string tableName = GetTableName(table.TableName); + + StatementBuilder builder; + builder.CreateTable(tableName).Columns({ + ColumnBuilder(table.ValueName, Type::Text).NotNull(), + ColumnBuilder(V2_0::details::SystemReferenceStringTableGetPrimaryColumnName(), Type::RowId).NotNull(), + ColumnBuilder(s_Delta_IsRemovedColumn, Type::Int64).NotNull(), + PrimaryKeyBuilder({ table.ValueName, V2_0::details::SystemReferenceStringTableGetPrimaryColumnName() }) + }).WithoutRowID(); + builder.Execute(connection); + } + + for (const auto& table : OneToManyTables()) + { + // The data table holds only values that the baseline does not already have. There is no + // removal flag because a value is only unreferenced once every map entry naming it is + // removed, which the map table already records. + { + std::string tableName = GetTableName(table.TableName); + + StatementBuilder builder; + builder.CreateTable(tableName).Columns({ + IntegerPrimaryKey(), + ColumnBuilder(table.ValueName, Type::Text).NotNull() + }); + builder.Execute(connection); + + // Generation looks values up by string to reuse an already allocated rowid. + StatementBuilder indexBuilder; + indexBuilder.CreateUniqueIndex({ tableName, s_Delta_ValueIndexSuffix }). + On(tableName).Columns(table.ValueName); + indexBuilder.Execute(connection); + } + + { + std::string mapTableName = GetMapTableName(table.TableName); + + StatementBuilder builder; + builder.CreateTable(mapTableName).Columns({ + ColumnBuilder(table.ValueName, Type::Int64).NotNull(), + ColumnBuilder(V2_0::details::OneToManyTableGetManifestColumnName(), Type::Int64).NotNull(), + ColumnBuilder(s_Delta_IsRemovedColumn, Type::Int64).NotNull(), + PrimaryKeyBuilder({ table.ValueName, V2_0::details::OneToManyTableGetManifestColumnName() }) + }).WithoutRowID(); + builder.Execute(connection); + } + } + + savepoint.Commit(); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h new file mode 100644 index 0000000000..e4be4c1727 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include +#include +#include + + +namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta +{ + // Describes a 2.0 table that associates string values with packages. + struct ValueTableInfo + { + std::string_view TableName; + std::string_view ValueName; + }; + + // The tables that store a value directly alongside the package that it refers to. + std::vector SystemReferenceTables(); + + // The tables that store values in a data table, associated with packages through a map table. + std::vector OneToManyTables(); + + // Gets the name of the delta table that mirrors the given 2.0 table. + // The delta tables are named distinctly so that a delta database can be attached alongside + // a baseline, and so that the merged views can take the 2.0 names for themselves. + std::string GetTableName(std::string_view baseTableName); + + // Gets the name of the delta map table that mirrors the map table of the given 2.0 table. + std::string GetMapTableName(std::string_view baseTableName); + + // The column that records a row as representing the removal of the data that it identifies. + std::string_view IsRemovedColumnName(); + + // Creates the full set of delta tables in the given database. + void CreateTables(SQLite::Connection& connection); +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h index 56258972ff..6740f560af 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h @@ -5,6 +5,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 { + // The point in time from which the next delta generated against this index should be computed. + static constexpr std::string_view s_MetadataValueName_DeltaBaselineTime = "deltaBaselineTime"sv; + // Interface to schema version 2.1 exposed through ISQLiteIndex. // Version 2.1 adds the is_removed column to the update_tracking table, // enabling delta index generation that can represent package removals. @@ -19,6 +22,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 bool MigrateFrom(SQLite::Connection& connection, const ISQLiteIndex* current) override; protected: + // Records the baseline time for this index, and generates a delta index against a previous + // baseline when the caller has supplied the paths to do so. + void CreateAdditionalPackagingOutput(const SQLiteIndexContext& context) override; + // Records removals in the update tracking table rather than deleting the row, // so that delta generation can see which packages have gone away. V2_0::PackageUpdateTrackingTable::RemovalBehavior GetTrackingRemovalBehavior() const override; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp index 9a70f4dccb..18471e8b82 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -3,6 +3,10 @@ #include "pch.h" #include "Interface.h" #include "Microsoft/Schema/2_0/PackageUpdateTrackingTable.h" +#include "Microsoft/Schema/2_1/DeltaGeneration.h" + +#include +#include namespace AppInstaller::Repository::Microsoft::Schema::V2_1 { @@ -36,6 +40,49 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 return false; } + void Interface::CreateAdditionalPackagingOutput(const SQLiteIndexContext& context) + { + SQLite::Connection& connection = context.Connection; + + // Record the point from which a delta against this index should be computed. Every 2.1 index + // does this, because any of them may later be designated as a baseline. + // TODO: We may need to set the baseline time to the max update tracking time +1 to only catch new incoming changes + // This assumes some delay between delta generation and the next package update. + // TODO: We also need to ensure that our times are UTC / not impacted by timezone shifts, etc. + SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_DeltaBaselineTime, std::to_string(Utility::GetCurrentUnixEpoch())); + + if (!context.Data.Contains(Property::DeltaBaselineIndexPath) || + !context.Data.Contains(Property::DeltaOutputPath)) + { + return; + } + + std::filesystem::path baselinePath = context.Data.Get(); + std::filesystem::path deltaOutputPath = context.Data.Get(); + + AICLI_LOG(Repo, Info, << "Generating a delta index against baseline [" << baselinePath << "]"); + + SQLite::Connection baselineConnection = SQLite::Connection::Create(baselinePath.u8string(), SQLite::Connection::OpenDisposition::ReadOnly); + + // The changes to capture are those written after the baseline recorded its own time. + int64_t baselineTime = 0; + std::optional baselineTimeString = SQLite::MetadataTable::TryGetNamedValue(baselineConnection, s_MetadataValueName_DeltaBaselineTime); + if (baselineTimeString && !baselineTimeString->empty()) + { + baselineTime = std::stoll(baselineTimeString.value()); + } + + auto changedPackages = V2_0::PackageUpdateTrackingTable::GetUpdatesSince(connection, baselineTime, GetTrackingRemovalBehavior()); + auto removedPackages = V2_0::PackageUpdateTrackingTable::GetRemovalsSince(connection, baselineTime, GetTrackingRemovalBehavior()); + + Delta::Generate( + connection, + baselineConnection, + deltaOutputPath, + changedPackages, + removedPackages); + } + V2_0::PackageUpdateTrackingTable::RemovalBehavior Interface::GetTrackingRemovalBehavior() const { return V2_0::PackageUpdateTrackingTable::RemovalBehavior::Record; From 8b355dcd05f1be84311666181e6c873dc09eaeb3 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Thu, 3 Sep 2026 13:47:54 -0700 Subject: [PATCH 19/36] Fixes --- src/AppInstallerCLITests/SQLiteIndex.cpp | 11 ++++++-- src/AppInstallerCLITests/SQLiteWrapper.cpp | 2 +- .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 28 +++++++++++++++++-- .../Microsoft/Schema/2_1/DeltaGeneration.h | 5 ++++ .../Microsoft/Schema/2_1/Interface_2_1.cpp | 1 + 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index 9d93fe108b..81d6669ae6 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -4033,7 +4033,7 @@ TEST_CASE("SQLiteIndex_PrepareForPackaging_RowIdsAreStable", "[sqliteindex][V2_0 REQUIRE(GetPreparedPackageRowId(secondFile.GetPath(), "Publisher4.Id") > GetPreparedPackageRowId(secondFile.GetPath(), "Publisher3.Id")); } -TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_RemovalDeletesRow", "[sqliteindex][V2_0][updatetracking]") +TEST_CASE("SQLiteIndex_UpdateTracking_V2_0_RemovalDeletesRow", "[sqliteindex][V2_0][updatetracking]") { TempFile indexFile{ "updatetracking"s, ".db"s }; @@ -4043,7 +4043,7 @@ TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_RemovalDeletesRow", "[sqliteindex][V2 CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); { - SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 1 }); + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 0 }); index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); index.AddManifest(m1.Manifest, m1.Path); index.AddManifest(m2.Manifest, m2.Path); @@ -4295,6 +4295,13 @@ TEST_CASE("SQLiteIndex_Delta_NoChanges_EmptyDelta", "[sqliteindex][V2_1][delta]" Connection deltaConn = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + // The delta is a database in its own right, carrying the metadata that any index has. Without + // it the delta could not be opened, since opening reads the schema version to pick an interface. + REQUIRE(MetadataTable::GetNamedValue(deltaConn, AppInstaller::SQLite::s_MetadataValueName_MajorVersion) == 2); + REQUIRE(MetadataTable::GetNamedValue(deltaConn, AppInstaller::SQLite::s_MetadataValueName_MinorVersion) == 1); + REQUIRE(!MetadataTable::TryGetNamedValue(deltaConn, AppInstaller::SQLite::s_MetadataValueName_DatabaseIdentifier).value_or(std::string{}).empty()); + REQUIRE(MetadataTable::TryGetNamedValue(deltaConn, AppInstaller::SQLite::s_MetadataValueName_LastWriteTime).has_value()); + for (std::string_view tableName : { "delta_packages"sv, "delta_pfns2"sv, "delta_productcodes2"sv, "delta_norm_names2"sv, "delta_norm_publishers2"sv, "delta_upgradecodes2"sv, diff --git a/src/AppInstallerCLITests/SQLiteWrapper.cpp b/src/AppInstallerCLITests/SQLiteWrapper.cpp index a7d32aa976..10cb0d2ae1 100644 --- a/src/AppInstallerCLITests/SQLiteWrapper.cpp +++ b/src/AppInstallerCLITests/SQLiteWrapper.cpp @@ -924,7 +924,7 @@ TEST_CASE("SQLBuilder_AttachAndTempView", "[sqlbuilder]") createTable.Execute(connection); Builder::StatementBuilder insert; - insert.InsertInto(deltaTable).Columns({ s_firstColumn, s_secondColumn }).Values(2, "delta"); + insert.InsertInto(deltaTable).Columns({ s_firstColumn, s_secondColumn }).Values(2, "delta"sv); insert.Execute(connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp index f6720455c7..8807c971b1 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -9,6 +9,7 @@ #include "Microsoft/Schema/2_0/SystemReferenceStringTable.h" #include +#include #include #include @@ -21,6 +22,27 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta namespace { + // The delta database is created through SQLiteStorageBase rather than as a bare connection + // so that it carries the same metadata as any other index: a schema version, a database + // identifier, and a last write time. Without that metadata it could not be opened, as + // opening reads the schema version to decide which interface to use. + struct DeltaDatabase : public SQLite::SQLiteStorageBase + { + DeltaDatabase(const std::filesystem::path& path, const SQLite::Version& version) : + SQLiteStorageBase(path.u8string(), version) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "delta_createdatabase_v2_1"); + + m_version.SetSchemaVersion(m_dbconn); + CreateTables(m_dbconn); + SetLastWriteTime(); + + savepoint.Commit(); + } + + SQLite::Connection& GetConnection() { return m_dbconn; } + }; + // Gets the rowid of the given package identifier in the packages table, if it is present. std::optional SelectPackageRowId(const SQLite::Connection& connection, const std::string& packageIdentifier) { @@ -276,15 +298,15 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta const SQLite::Connection& sourceConnection, const SQLite::Connection& baselineConnection, const std::filesystem::path& deltaOutputPath, + const SQLite::Version& version, const std::vector& changedPackages, const std::vector& removedPackages) { AICLI_LOG(Repo, Info, << "Generating delta index at [" << deltaOutputPath << "] for " << changedPackages.size() << " changed and " << removedPackages.size() << " removed packages"); - SQLite::Connection deltaConnection = SQLite::Connection::Create(deltaOutputPath.u8string(), SQLite::Connection::OpenDisposition::Create); - - CreateTables(deltaConnection); + DeltaDatabase deltaDatabase{ deltaOutputPath, version }; + SQLite::Connection& deltaConnection = deltaDatabase.GetConnection(); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(deltaConnection, "delta_generate_v2_1"); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h index 8102a9aee2..7b5ac960db 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h @@ -3,6 +3,7 @@ #pragma once #include "Microsoft/Schema/2_0/PackageUpdateTrackingTable.h" #include +#include #include #include #include @@ -19,10 +20,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta // The source and the baseline assign the same rowid to a given package, so a package that // exists in both is described by rows that carry its baseline rowid, and a package that is new // to the source carries a rowid that the baseline cannot have used. + // + // The version is recorded as the delta's own schema version, so that opening the delta selects + // the interface that knows how to merge it with a baseline. void Generate( const SQLite::Connection& sourceConnection, const SQLite::Connection& baselineConnection, const std::filesystem::path& deltaOutputPath, + const SQLite::Version& version, const std::vector& changedPackages, const std::vector& removedPackages); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp index 18471e8b82..48b538fa54 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -79,6 +79,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 connection, baselineConnection, deltaOutputPath, + GetVersion(), changedPackages, removedPackages); } From 27c60c46bdd9089d3605120775684ee8a8404181 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Thu, 3 Sep 2026 13:52:48 -0700 Subject: [PATCH 20/36] Revert test change --- src/AppInstallerCLITests/SQLiteIndex.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index 81d6669ae6..d94d3a6045 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -4060,7 +4060,7 @@ TEST_CASE("SQLiteIndex_UpdateTracking_V2_0_RemovalDeletesRow", "[sqliteindex][V2 REQUIRE(updates[0].PackageIdentifier == "Publisher1.Id"); // Read with Record to check on the Delete - REQUIRE(Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record).empty()); + REQUIRE(Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Delete).empty()); } TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_RemovalIsRecorded", "[sqliteindex][V2_1][updatetracking]") From b3348874e4e17ee1c6aea7468252c2ddb6ca1cb6 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Thu, 3 Sep 2026 15:42:34 -0700 Subject: [PATCH 21/36] Create read views --- src/AppInstallerCLITests/SQLiteIndex.cpp | 77 ++++++++ src/AppInstallerCLITests/SQLiteWrapper.cpp | 70 +++++++ .../AppInstallerRepositoryCore.vcxproj | 2 + ...AppInstallerRepositoryCore.vcxproj.filters | 6 + .../Microsoft/Schema/2_0/Interface.h | 9 +- .../Microsoft/Schema/2_0/Interface_2_0.cpp | 77 -------- .../Schema/2_0/OneToManyTableWithMap.cpp | 7 + .../Microsoft/Schema/2_1/DeltaViews.cpp | 185 ++++++++++++++++++ .../Microsoft/Schema/2_1/DeltaViews.h | 18 ++ .../Microsoft/Schema/2_1/Interface.h | 5 + .../Microsoft/Schema/2_1/Interface_2_1.cpp | 12 ++ .../Public/winget/SQLiteStatementBuilder.h | 9 + .../SQLiteStatementBuilder.cpp | 12 ++ 13 files changed, 406 insertions(+), 83 deletions(-) create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index d94d3a6045..3e167fb3c7 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -4315,6 +4316,82 @@ TEST_CASE("SQLiteIndex_Delta_NoChanges_EmptyDelta", "[sqliteindex][V2_1][delta]" } } +// Reads a package's tags through the merged delta views. This is the shape that the 2.0 search +// path reads in: the map table governs which values a package has, and the value table holds the +// strings themselves. +std::set GetTagsThroughDeltaViews(Connection& connection, std::string_view packageId) +{ + std::set result; + + Statement statement = Statement::Create(connection, + "SELECT [t].[tag] FROM [tags2] AS [t] " + "JOIN [tags2_map] AS [m] ON [m].[tag] = [t].[rowid] " + "JOIN [packages] AS [p] ON [p].[rowid] = [m].[package] " + "WHERE [p].[id] = ?"); + statement.Bind(1, std::string{ packageId }); + + while (statement.Step()) + { + result.insert(statement.GetColumn(0)); + } + + return result; +} + +TEST_CASE("SQLiteIndex_Delta_MergedViews_AssociationsAreSuppressedPerRow", "[sqliteindex][V2_1][delta]") +{ + TempFile workingFile{ "delta_working"s, ".db"s }; + TempFile baselineFile{ "delta_baseline"s, ".db"s }; + TempFile deltaFile{ "delta_output"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + // Both packages start out with the tags that a fake manifest carries: t1 and t2. + { + SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + + std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + } + + // Publisher1 trades t2 for t3, keeping t1. Publisher2 goes away entirely. + { + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + m1.Manifest.DefaultLocalization.Add({ "t1", "t3" }); + REQUIRE(index.UpdateManifest(m1.Manifest, m1.Path)); + + index.RemoveManifest(m2.Manifest, m2.Path); + + index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); + index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); + index.PrepareForPackaging(); + } + + REQUIRE(std::filesystem::exists(deltaFile.GetPath())); + + Connection deltaConnection = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + Schema::V2_1::Delta::SetupReadMode(deltaConnection, baselineFile.GetPath()); + + // The delta records only what changed about Publisher1, so it never mentions t1 at all. + // Suppressing the baseline at the level of the package would therefore lose it. + REQUIRE(GetTagsThroughDeltaViews(deltaConnection, "Publisher1.Id") == std::set{ "t1", "t3" }); + + // A removed package gets no per-association removal rows, so the only thing that can suppress + // its associations is the removal recorded against the package itself. + REQUIRE(GetTagsThroughDeltaViews(deltaConnection, "Publisher2.Id").empty()); +} + TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_Search", "[sqliteindex][V2_1][delta]") { TempFile workingFile{ "delta_working"s, ".db"s }; diff --git a/src/AppInstallerCLITests/SQLiteWrapper.cpp b/src/AppInstallerCLITests/SQLiteWrapper.cpp index 10cb0d2ae1..f7a299ff26 100644 --- a/src/AppInstallerCLITests/SQLiteWrapper.cpp +++ b/src/AppInstallerCLITests/SQLiteWrapper.cpp @@ -964,6 +964,76 @@ TEST_CASE("SQLBuilder_AttachAndTempView", "[sqlbuilder]") } } +TEST_CASE("SQLBuilder_ViewWithTombstoneSuppression", "[sqlbuilder]") +{ + Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); + + constexpr std::string_view valueTombstoneTable = "valuetombstone"; + constexpr std::string_view ownerTombstoneTable = "ownertombstone"; + constexpr std::string_view removedColumn = "is_removed"; + constexpr std::string_view valueAlias = "v"; + constexpr std::string_view ownerAlias = "o"; + constexpr std::string_view viewName = "survivors"; + + CreateSimpleTestTable(connection); + InsertIntoSimpleTestTable(connection, 1, "kept"); + InsertIntoSimpleTestTable(connection, 2, "value removed"); + InsertIntoSimpleTestTable(connection, 3, "owner removed"); + + auto createTombstoneTable = [&](std::string_view tableName, int suppressedValue) + { + Builder::StatementBuilder createTable; + createTable.CreateTable(tableName).Columns({ + Builder::ColumnBuilder(s_firstColumn, Builder::Type::Int), + Builder::ColumnBuilder(removedColumn, Builder::Type::Int), + }); + createTable.Execute(connection); + + Builder::StatementBuilder insertSuppressed; + insertSuppressed.InsertInto(tableName).Columns({ s_firstColumn, removedColumn }).Values(suppressedValue, 1); + insertSuppressed.Execute(connection); + + // Row 1 is named by both tables without being removed by either, which is what + // distinguishes a test of the removal flag from a test of mere presence. + Builder::StatementBuilder insertMentioned; + insertMentioned.InsertInto(tableName).Columns({ s_firstColumn, removedColumn }).Values(1, 0); + insertMentioned.Execute(connection); + }; + + createTombstoneTable(valueTombstoneTable, 2); + createTombstoneTable(ownerTombstoneTable, 3); + + { + INFO("A view cannot contain bound parameters, so its comparisons have to be literals"); + Builder::StatementBuilder createView; + createView.CreateTempView(viewName). + Select({ s_firstColumn, s_secondColumn }).From(s_tableName). + Where().NotExists().BeginParenthetical(). + Select(s_firstColumn).From(valueTombstoneTable).As(valueAlias). + Where(Builder::QualifiedColumn{ valueAlias, s_firstColumn }).Equals(Builder::QualifiedColumn{ s_tableName, s_firstColumn }). + And(Builder::QualifiedColumn{ valueAlias, removedColumn }).EqualsLiteral(1). + EndParenthetical(). + And().NotExists().BeginParenthetical(). + Select(s_firstColumn).From(ownerTombstoneTable).As(ownerAlias). + Where(Builder::QualifiedColumn{ ownerAlias, s_firstColumn }).Equals(Builder::QualifiedColumn{ s_tableName, s_firstColumn }). + And(Builder::QualifiedColumn{ ownerAlias, removedColumn }).EqualsLiteral(1). + EndParenthetical(); + createView.Execute(connection); + } + + INFO("Only the row that neither tombstone removes survives"); + Builder::StatementBuilder select; + select.Select({ s_firstColumn, s_secondColumn }).From(viewName).OrderBy(s_firstColumn); + + Statement statement = select.Prepare(connection); + + REQUIRE(statement.Step()); + REQUIRE(statement.GetColumn(0) == 1); + REQUIRE(statement.GetColumn(1) == "kept"); + + REQUIRE(!statement.Step()); +} + TEST_CASE("SQLiteWrapperTransactionRollback", "[sqlitewrapper]") { Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index 71172fabb9..3f69f65c86 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -333,6 +333,7 @@ + @@ -448,6 +449,7 @@ + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index 25d88eb6d8..2071579398 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -528,6 +528,9 @@ Microsoft\Schema\2_1 + + Microsoft\Schema\2_1 + Microsoft\Schema\2_1 @@ -830,6 +833,9 @@ Microsoft\Schema\2_1 + + Microsoft\Schema\2_1 + Microsoft\Schema\2_1 diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h index 0372a838a5..5ac38de8ad 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h @@ -56,11 +56,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 bool MigrateFrom(SQLite::Connection& connection, const ISQLiteIndex* current) override; void SetProperty(SQLite::Connection& connection, Property property, const std::string& value) override; - // Sets up this index to act as a composed (delta + baseline) read-only view. - // Attaches the baseline database and creates TEMP VIEWs that union delta + baseline data. - // Must be called before any read operations on a delta index. - void SetupDeltaReadMode(SQLite::Connection& connection, const std::filesystem::path& baselinePath); - protected: // Determines how the removal of a package is recorded in the update tracking table. // Version 2.0 deletes the row; later versions may record the removal instead so that @@ -106,7 +101,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // If EnsureInternalInterface has been called. mutable bool m_internalInterfaceChecked = false; - // Set to true after SetupDeltaReadMode; prevents EnsureInternalInterface from creating the V1.7 interface. + // Set when the tables that this interface reads are the merged views over a delta and its + // baseline rather than tables of this database. Version 2.0 cannot produce that state + // itself; a derived version sets this when it establishes the views. mutable bool m_isDeltaReadMode = false; // Interface to the data before PrepareForPackaging is called. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index 47195d407d..aa64a7eed9 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -806,81 +806,4 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { return CreateISQLiteIndex({ 1, 7 }); } - - void Interface::SetupDeltaReadMode(SQLite::Connection& connection, const std::filesystem::path& baselinePath) - { - AICLI_LOG(Repo, Info, << "Setting up delta read mode with baseline [" << baselinePath << "]"); - - // Attach the baseline database under the "baseline" schema name - { - std::string attachSql = "ATTACH DATABASE ? AS baseline"; - SQLite::Statement stmt = SQLite::Statement::Create(connection, attachSql); - stmt.Bind(1, baselinePath.u8string()); - stmt.Execute(); - } - - // TEMP VIEW: packages - // Delta entries (added/updated) override baseline; removed packages are excluded. - SQLite::Statement::Create(connection, R"( - CREATE TEMP VIEW packages AS - SELECT rowid, id, name, moniker, latest_version, arp_min_version, arp_max_version, hash - FROM delta_packages WHERE is_removed = 0 - UNION ALL - SELECT p.rowid, p.id, p.name, p.moniker, p.latest_version, p.arp_min_version, p.arp_max_version, p.hash - FROM baseline.packages p - WHERE p.id NOT IN (SELECT id FROM delta_packages) - )").Execute(); - - // TEMP VIEWs: SystemReference tables (pfns2, productcodes2, norm_names2, norm_publishers2, upgradecodes2) - // For changed packages: delta has the full current set (is_removed=0 = current, is_removed=1 = removed). - // For unchanged packages: baseline rows pass through. - static constexpr std::pair s_SysRefTables[] = { - { "pfns2", "pfn" }, - { "productcodes2", "productcode" }, - { "norm_names2", "norm_name" }, - { "norm_publishers2", "norm_publisher" }, - { "upgradecodes2", "upgradecode" }, - }; - for (const auto& [table, value] : s_SysRefTables) - { - std::string sql = - "CREATE TEMP VIEW " + std::string(table) + " AS " - "SELECT " + std::string(value) + ", package FROM delta_" + std::string(table) + " WHERE is_removed = 0 " - "UNION ALL " - "SELECT " + std::string(value) + ", package FROM baseline." + std::string(table) + " " - "WHERE package NOT IN (SELECT package FROM delta_" + std::string(table) + ")"; - SQLite::Statement::Create(connection, sql).Execute(); - } - - // TEMP VIEW: tags2 / commands2 data tables (rowid + value). - // Delta only contains NEW strings (with rowids > baseline max); no conflicts possible. - static constexpr std::pair s_OneToManyTables[] = { - { "tags2", "tag" }, - { "commands2", "command" }, - }; - for (const auto& [table, value] : s_OneToManyTables) - { - std::string sql = - "CREATE TEMP VIEW " + std::string(table) + " AS " - "SELECT rowid, " + std::string(value) + " FROM delta_" + std::string(table) + " " - "UNION ALL " - "SELECT rowid, " + std::string(value) + " FROM baseline." + std::string(table); - SQLite::Statement::Create(connection, sql).Execute(); - - // TEMP VIEW: tags2_map / commands2_map - // For changed packages: delta has the full current set of mappings. - // For unchanged packages: baseline mappings pass through. - std::string mapSql = - "CREATE TEMP VIEW " + std::string(table) + "_map AS " - "SELECT " + std::string(value) + ", package FROM delta_" + std::string(table) + "_map WHERE is_removed = 0 " - "UNION ALL " - "SELECT bm." + std::string(value) + ", bm.package " - "FROM baseline." + std::string(table) + "_map bm " - "WHERE bm.package NOT IN (SELECT package FROM delta_" + std::string(table) + "_map)"; - SQLite::Statement::Create(connection, mapSql).Execute(); - } - - m_isDeltaReadMode = true; - m_internalInterfaceChecked = true; // Suppress normal EnsureInternalInterface logic - } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp index a1ec81e301..2739282285 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp @@ -23,6 +23,13 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return s_OneToManyTableWithMap_MapTable_PrimaryName; } + std::string OneToManyTableGetMapTableName(std::string_view tableName) + { + std::string result{ tableName }; + result += s_OneToManyTableWithMap_MapTable_Suffix; + return result; + } + namespace anon { // Create the mapping table insert statement for multiple use. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp new file mode 100644 index 0000000000..414066a9df --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/Schema/2_1/DeltaViews.h" +#include "Microsoft/Schema/2_1/DeltaTables.h" + +#include "Microsoft/Schema/2_0/PackagesTable.h" +#include "Microsoft/Schema/2_0/OneToManyTableWithMap.h" +#include "Microsoft/Schema/2_0/SystemReferenceStringTable.h" + +#include + +#include + + +namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta +{ + using namespace std::string_view_literals; + using namespace SQLite::Builder; + + namespace + { + // The schema name that the baseline database is attached under. + constexpr std::string_view s_Delta_BaselineSchema = "baseline"sv; + + // Aliases for the two sides of a merge. The correlated subqueries below name the same + // table on both sides, so the aliases are what keep the two references apart. + constexpr std::string_view s_Delta_BaselineAlias = "b"sv; + constexpr std::string_view s_Delta_DeltaAlias = "d"sv; + constexpr std::string_view s_Delta_PackagesAlias = "p"sv; + + // Appends a test that the package owning the current baseline row still exists. + // + // When a package is removed, the delta records that fact once, in its packages table; it + // does not write a removal row for each of the package's associations. This is therefore + // the only thing standing between a removed package and its associations continuing to + // appear in the merged data. + void AppendPackageNotRemoved(StatementBuilder& builder, std::string_view packageColumn) + { + builder.NotExists().BeginParenthetical(). + Select(SQLite::RowIDName). + From(GetTableName(V2_0::PackagesTable::TableName())).As(s_Delta_PackagesAlias). + Where(QualifiedColumn{ s_Delta_PackagesAlias, SQLite::RowIDName }). + Equals(QualifiedColumn{ s_Delta_BaselineAlias, packageColumn }). + And(QualifiedColumn{ s_Delta_PackagesAlias, IsRemovedColumnName() }).EqualsLiteral(1). + EndParenthetical(); + } + + // Creates the view that merges the packages themselves. + // + // The delta holds a row for every package that changed and every package that was removed, + // both keyed by the rowid that the package also has in the baseline. A baseline package is + // therefore superseded whenever the delta mentions its rowid at all: the delta row replaces + // it when it changed, and stands for its absence when it was removed. + void CreatePackagesView(SQLite::Connection& connection) + { + std::string deltaPackages = GetTableName(V2_0::PackagesTable::TableName()); + + StatementBuilder builder; + builder.CreateTempView(V2_0::PackagesTable::TableName()). + Select({ + SQLite::RowIDName, + V2_0::PackagesTable::IdColumn::Name, + V2_0::PackagesTable::NameColumn::Name, + V2_0::PackagesTable::MonikerColumn::Name, + V2_0::PackagesTable::LatestVersionColumn::Name, + V2_0::PackagesTable::ARPMinVersionColumn::Name, + V2_0::PackagesTable::ARPMaxVersionColumn::Name, + V2_0::PackagesTable::HashColumn::Name }). + From(deltaPackages). + Where(IsRemovedColumnName()).EqualsLiteral(0). + UnionAll(). + Select({ + QualifiedColumn{ s_Delta_BaselineAlias, SQLite::RowIDName }, + QualifiedColumn{ s_Delta_BaselineAlias, V2_0::PackagesTable::IdColumn::Name }, + QualifiedColumn{ s_Delta_BaselineAlias, V2_0::PackagesTable::NameColumn::Name }, + QualifiedColumn{ s_Delta_BaselineAlias, V2_0::PackagesTable::MonikerColumn::Name }, + QualifiedColumn{ s_Delta_BaselineAlias, V2_0::PackagesTable::LatestVersionColumn::Name }, + QualifiedColumn{ s_Delta_BaselineAlias, V2_0::PackagesTable::ARPMinVersionColumn::Name }, + QualifiedColumn{ s_Delta_BaselineAlias, V2_0::PackagesTable::ARPMaxVersionColumn::Name }, + QualifiedColumn{ s_Delta_BaselineAlias, V2_0::PackagesTable::HashColumn::Name } }). + From(QualifiedTable{ s_Delta_BaselineSchema, V2_0::PackagesTable::TableName() }).As(s_Delta_BaselineAlias). + Where().NotExists().BeginParenthetical(). + Select(SQLite::RowIDName).From(deltaPackages).As(s_Delta_DeltaAlias). + Where(QualifiedColumn{ s_Delta_DeltaAlias, SQLite::RowIDName }). + Equals(QualifiedColumn{ s_Delta_BaselineAlias, SQLite::RowIDName }). + EndParenthetical(); + + builder.Execute(connection); + } + + // Creates the view that merges a table associating values with packages. This covers both + // the system reference tables, which hold the value inline, and the one to many map tables, + // which hold a reference to it; the two have the same shape as far as merging is concerned. + // + // The delta records only the associations that changed, rather than the full current set + // for a changed package. A baseline association therefore survives unless the delta names + // that exact pair, or the package it belongs to has gone away entirely. Suppressing at the + // level of the package instead would discard every association a changed package still has. + void CreateAssociationView( + SQLite::Connection& connection, + std::string_view viewName, + const std::string& deltaTableName, + std::string_view valueColumn, + std::string_view packageColumn) + { + StatementBuilder builder; + builder.CreateTempView(viewName). + Select({ valueColumn, packageColumn }). + From(deltaTableName). + Where(IsRemovedColumnName()).EqualsLiteral(0). + UnionAll(). + Select({ + QualifiedColumn{ s_Delta_BaselineAlias, valueColumn }, + QualifiedColumn{ s_Delta_BaselineAlias, packageColumn } }). + From(QualifiedTable{ s_Delta_BaselineSchema, viewName }).As(s_Delta_BaselineAlias). + Where().NotExists().BeginParenthetical(). + Select(packageColumn).From(deltaTableName).As(s_Delta_DeltaAlias). + Where(QualifiedColumn{ s_Delta_DeltaAlias, valueColumn }). + Equals(QualifiedColumn{ s_Delta_BaselineAlias, valueColumn }). + And(QualifiedColumn{ s_Delta_DeltaAlias, packageColumn }). + Equals(QualifiedColumn{ s_Delta_BaselineAlias, packageColumn }). + EndParenthetical(). + And(); + + AppendPackageNotRemoved(builder, packageColumn); + + builder.Execute(connection); + } + + // Creates the view that merges a one to many data table. + // + // Nothing is ever suppressed here. The delta only contains values that the baseline has + // never held, and it numbers them above the baseline's highest rowid, so the two sets are + // disjoint. A value that no package refers to any more is left in place; the map table + // governs what is visible, and an unreferenced value simply never appears. + void CreateValueView(SQLite::Connection& connection, std::string_view tableName, std::string_view valueColumn) + { + StatementBuilder builder; + builder.CreateTempView(tableName). + Select({ SQLite::RowIDName, valueColumn }). + From(GetTableName(tableName)). + UnionAll(). + Select({ SQLite::RowIDName, valueColumn }). + From(QualifiedTable{ s_Delta_BaselineSchema, tableName }); + + builder.Execute(connection); + } + } + + void SetupReadMode(SQLite::Connection& connection, const std::string& baselinePath) + { + AICLI_LOG(Repo, Info, << "Setting up delta read mode with baseline [" << baselinePath << "]"); + + { + StatementBuilder builder; + builder.Attach(baselinePath, s_Delta_BaselineSchema); + builder.Execute(connection); + } + + CreatePackagesView(connection); + + for (const auto& table : SystemReferenceTables()) + { + CreateAssociationView( + connection, + table.TableName, + GetTableName(table.TableName), + table.ValueName, + V2_0::details::SystemReferenceStringTableGetPrimaryColumnName()); + } + + for (const auto& table : OneToManyTables()) + { + CreateValueView(connection, table.TableName, table.ValueName); + + CreateAssociationView( + connection, + V2_0::details::OneToManyTableGetMapTableName(table.TableName), + GetMapTableName(table.TableName), + table.ValueName, + V2_0::details::OneToManyTableGetManifestColumnName()); + } + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h new file mode 100644 index 0000000000..0a570234bf --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include + +#include + + +namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta +{ + // Attaches the given baseline database to the delta database that the connection is open on, + // then defines a temporary view for each 2.0 table that presents the combination of the two. + // + // The views take the 2.0 table names for themselves, and the delta's own tables are named + // distinctly, so every 2.0 read path operates on the merged data without knowing that it is + // merged. The views are temporary, so they last only as long as the connection. + void SetupReadMode(SQLite::Connection& connection, const std::string& baselinePath); +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h index 6740f560af..60c0efc9e0 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h @@ -21,6 +21,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 // Version 2.0 bool MigrateFrom(SQLite::Connection& connection, const ISQLiteIndex* current) override; + // Sets this index up to read the combination of a delta and the baseline it was generated + // against. Attaches the baseline and defines the merged views, after which every inherited + // read path operates on the combination. Must be called before any read. + void SetupDeltaReadMode(SQLite::Connection& connection, const std::string& baselinePath); + protected: // Records the baseline time for this index, and generates a delta index against a previous // baseline when the caller has supplied the paths to do so. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp index 48b538fa54..b73e655c39 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -4,6 +4,7 @@ #include "Interface.h" #include "Microsoft/Schema/2_0/PackageUpdateTrackingTable.h" #include "Microsoft/Schema/2_1/DeltaGeneration.h" +#include "Microsoft/Schema/2_1/DeltaViews.h" #include #include @@ -40,6 +41,17 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 return false; } + void Interface::SetupDeltaReadMode(SQLite::Connection& connection, const std::string& baselinePath) + { + Delta::SetupReadMode(connection, baselinePath); + + // The merged data is presented through views rather than tables, so the checks that the + // base makes to decide whether this index has been packaged cannot see it. Record that the + // question is already settled: a delta is only ever read, and only in its packaged form. + m_isDeltaReadMode = true; + m_internalInterfaceChecked = true; + } + void Interface::CreateAdditionalPackagingOutput(const SQLiteIndexContext& context) { SQLite::Connection& connection = context.Connection; diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h index fede0c3311..0f791ceb64 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h @@ -292,6 +292,11 @@ namespace AppInstaller::SQLite::Builder // `value = NULL` is always false when used as a filter. Use IsNull for filtering. StatementBuilder& AssignValue(std::nullptr_t); + // Compares the current column against the value written directly into the statement + // rather than bound to it. A view definition cannot contain bound parameters, so a + // comparison within one has to be expressed this way. + StatementBuilder& EqualsLiteral(int64_t value); + template StatementBuilder& IsGreaterThan(const ValueType& value) { @@ -334,6 +339,10 @@ namespace AppInstaller::SQLite::Builder StatementBuilder& And(const QualifiedColumn& column); StatementBuilder& Or(const QualifiedColumn& column); + // Continues a filter clause with a condition that is not introduced by a column, + // such as a further `not exists` subquery. + StatementBuilder& And(); + // Begin a join clause. // The initializer_list form enables the table name to be constructed from multiple parts. StatementBuilder& Join(std::string_view table); diff --git a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp index 4363abe8a1..19692e2b1d 100644 --- a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp +++ b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp @@ -365,6 +365,12 @@ namespace AppInstaller::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::EqualsLiteral(int64_t value) + { + m_stream << " = " << value; + return *this; + } + StatementBuilder& StatementBuilder::Equals() { m_stream << " ="; @@ -469,6 +475,12 @@ namespace AppInstaller::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::And() + { + m_stream << " AND"; + return *this; + } + StatementBuilder& StatementBuilder::Join(std::string_view table) { OutputOperationAndTable(m_stream, " JOIN", table); From a2bc7ae15bbf6dc68cbaad7ee85e2bd7ad8476b3 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Thu, 3 Sep 2026 16:01:42 -0700 Subject: [PATCH 22/36] Prepare delta and move to property over virtual --- src/AppInstallerCLITests/SQLiteIndex.cpp | 8 +++- .../Microsoft/Schema/2_0/Interface.h | 11 ++--- .../Microsoft/Schema/2_0/Interface_2_0.cpp | 21 ++++------ .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 3 ++ .../Microsoft/Schema/2_1/DeltaTables.cpp | 41 ++++++++++++++++++- .../Microsoft/Schema/2_1/DeltaTables.h | 4 ++ .../Microsoft/Schema/2_1/Interface.h | 4 -- .../Microsoft/Schema/2_1/Interface_2_1.cpp | 16 ++++---- 8 files changed, 76 insertions(+), 32 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index 3e167fb3c7..b2ba3d1223 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -4314,6 +4314,12 @@ TEST_CASE("SQLiteIndex_Delta_NoChanges_EmptyDelta", "[sqliteindex][V2_1][delta]" REQUIRE(countStmt.Step()); REQUIRE(countStmt.GetColumn(0) == 0); } + + // The delta ships as plain tables, just as a prepared 2.0 index does. Its indexes serve only + // generation, and the merged views probe nothing but primary keys. + Statement indexStmt = Statement::Create(deltaConn, "SELECT COUNT(*) FROM [sqlite_master] WHERE [type] = 'index'"); + REQUIRE(indexStmt.Step()); + REQUIRE(indexStmt.GetColumn(0) == 0); } // Reads a package's tags through the merged delta views. This is the shape that the 2.0 search @@ -4381,7 +4387,7 @@ TEST_CASE("SQLiteIndex_Delta_MergedViews_AssociationsAreSuppressedPerRow", "[sql REQUIRE(std::filesystem::exists(deltaFile.GetPath())); Connection deltaConnection = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); - Schema::V2_1::Delta::SetupReadMode(deltaConnection, baselineFile.GetPath()); + Schema::V2_1::Delta::SetupReadMode(deltaConnection, baselineFile.GetPath().u8string()); // The delta records only what changed about Publisher1, so it never mentions t1 at all. // Suppressing the baseline at the level of the package would therefore lose it. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h index 5ac38de8ad..4532c942c7 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h @@ -57,11 +57,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 void SetProperty(SQLite::Connection& connection, Property property, const std::string& value) override; protected: - // Determines how the removal of a package is recorded in the update tracking table. - // Version 2.0 deletes the row; later versions may record the removal instead so that - // a delta index can express it. - virtual PackageUpdateTrackingTable::RemovalBehavior GetTrackingRemovalBehavior() const; - // Creates the search results table. virtual std::unique_ptr CreateSearchResultsTable(const SQLite::Connection& connection) const; @@ -101,6 +96,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // If EnsureInternalInterface has been called. mutable bool m_internalInterfaceChecked = false; + // Determines how the removal of a package is recorded in the update tracking table. + // Version 2.0 deletes the row; a derived version sets this to record the removal instead, + // so that a delta index can express it. This varies only by schema version, so the + // constructor of that version establishes it rather than a virtual answering per call. + PackageUpdateTrackingTable::RemovalBehavior m_trackingRemovalBehavior = PackageUpdateTrackingTable::RemovalBehavior::Delete; + // Set when the tables that this interface reads are the merged views over a delta and its // baseline rather than tables of this database. Version 2.0 cannot produce that state // itself; a derived version sets this when it establishes the views. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index aa64a7eed9..c0a2b8c921 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -107,7 +107,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { EnsureInternalInterface(connection, true); SQLite::rowid_t manifestId = m_internalInterface->AddManifest(connection, manifest, relativePath); - PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, manifestId, PackageVersionProperty::Id).value(), GetTrackingRemovalBehavior()); + PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, manifestId, PackageVersionProperty::Id).value(), m_trackingRemovalBehavior); return manifestId; } @@ -117,7 +117,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 std::pair result = m_internalInterface->UpdateManifest(connection, manifest, relativePath); if (result.first) { - PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, result.second, PackageVersionProperty::Id).value(), GetTrackingRemovalBehavior()); + PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), m_internalInterface->GetPropertyByPrimaryId(connection, result.second, PackageVersionProperty::Id).value(), m_trackingRemovalBehavior); } return result; } @@ -143,7 +143,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 m_internalInterface->RemoveManifestById(connection, manifestId); if (identifier) { - PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), identifier.value(), GetTrackingRemovalBehavior()); + PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), identifier.value(), m_trackingRemovalBehavior); } } @@ -174,7 +174,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (m_internalInterface) { AICLI_CHECK_CONSISTENCY(m_internalInterface->CheckConsistency(connection, log)); - AICLI_CHECK_CONSISTENCY(PackageUpdateTrackingTable::CheckConsistency(connection, m_internalInterface.get(), GetTrackingRemovalBehavior(), log)); + AICLI_CHECK_CONSISTENCY(PackageUpdateTrackingTable::CheckConsistency(connection, m_internalInterface.get(), m_trackingRemovalBehavior, log)); return result; } @@ -398,14 +398,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migrate_from_v2_0"); // We only need to insert all of the existing packages into the update tracking table. - PackageUpdateTrackingTable::EnsureExists(connection, GetTrackingRemovalBehavior()); + PackageUpdateTrackingTable::EnsureExists(connection, m_trackingRemovalBehavior); SearchResult allPackages = current->Search(connection, {}); for (const auto& packageMatch : allPackages.Matches) { std::vector versionKeys = current->GetVersionKeysById(connection, packageMatch.first); ISQLiteIndex::VersionKey& latestVersionKey = versionKeys[0]; - PackageUpdateTrackingTable::Update(connection, current, current->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Id).value(), GetTrackingRemovalBehavior(), false); + PackageUpdateTrackingTable::Update(connection, current, current->GetPropertyByPrimaryId(connection, latestVersionKey.ManifestId, PackageVersionProperty::Id).value(), m_trackingRemovalBehavior, false); } savepoint.Commit(); @@ -440,11 +440,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { } - PackageUpdateTrackingTable::RemovalBehavior Interface::GetTrackingRemovalBehavior() const - { - return PackageUpdateTrackingTable::RemovalBehavior::Delete; - } - std::unique_ptr Interface::CreateSearchResultsTable(const SQLite::Connection& connection) const { return std::make_unique(connection); @@ -655,10 +650,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 THROW_WIN32_IF(ERROR_INVALID_STATE, baseOutputDirectory.empty() || baseOutputDirectory.is_relative()); // TEMP - PackageUpdateTrackingTable::EnsureExists(connection, GetTrackingRemovalBehavior()); + PackageUpdateTrackingTable::EnsureExists(connection, m_trackingRemovalBehavior); // Output all of the changed package version manifests since the base time to the target location - for (const auto& packageData : PackageUpdateTrackingTable::GetUpdatesSince(connection, updateBaseTime, GetTrackingRemovalBehavior())) + for (const auto& packageData : PackageUpdateTrackingTable::GetUpdatesSince(connection, updateBaseTime, m_trackingRemovalBehavior)) { std::filesystem::path packageDirectory = baseOutputDirectory / Manifest::PackageVersionDataManifest::GetRelativeDirectoryPath(packageData.PackageIdentifier, Utility::SHA256::ConvertToString(packageData.Hash)); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp index 8807c971b1..51b65fe3ed 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -358,6 +358,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta savepoint.Commit(); + // Outside the savepoint, since this vacuums. + PrepareTablesForPackaging(deltaConnection); + AICLI_LOG(Repo, Info, << "Delta index generation complete"); } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp index ed3ba42fa8..510d46698b 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp @@ -97,7 +97,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta }); builder.Execute(connection); - // The merged view excludes baseline packages by identifier, so that lookup must be fast. + // Generation must never write two rows for the same package, and the delta is small + // enough that the index costs little while it is being built. It is dropped again + // before the delta ships; see PrepareTablesForPackaging. StatementBuilder indexBuilder; indexBuilder.CreateUniqueIndex({ tableName, s_Delta_ValueIndexSuffix }). On(tableName).Columns(V2_0::PackagesTable::IdColumn::Name); @@ -157,4 +159,41 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta savepoint.Commit(); } + + void PrepareTablesForPackaging(SQLite::Connection& connection) + { + using namespace SQLite::Builder; + + // Every index here exists only to serve generation: the one on the packages table enforces + // that a package is recorded once, and those on the one to many data tables let generation + // find the rowid it already allocated for a value. Nothing reads them. + // + // The merged views need no index at all. They suppress baseline packages by rowid, and + // baseline associations by the (value, package) pair that is the primary key of a WITHOUT + // ROWID table, so every probe already lands on a key. This matches the 2.0 index itself, + // which drops all of its indexes in PrepareForPackaging and ships as plain tables. + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "delta_preparetables_v2_1"); + + StatementBuilder packagesBuilder; + packagesBuilder.DropIndex({ GetTableName(V2_0::PackagesTable::TableName()), s_Delta_ValueIndexSuffix }); + packagesBuilder.Execute(connection); + + for (const auto& table : OneToManyTables()) + { + StatementBuilder builder; + builder.DropIndex({ GetTableName(table.TableName), s_Delta_ValueIndexSuffix }); + builder.Execute(connection); + } + + savepoint.Commit(); + } + + // Generation only ever inserts, so there is nothing to reclaim from the data itself. The + // indexes just dropped are the exception, and the whole point of a delta is the bytes it + // costs to deliver, so it is worth returning those pages to the file. + StatementBuilder vacuumBuilder; + vacuumBuilder.Vacuum(); + vacuumBuilder.Execute(connection); + } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h index e4be4c1727..c26a9e7726 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h @@ -35,4 +35,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta // Creates the full set of delta tables in the given database. void CreateTables(SQLite::Connection& connection); + + // Drops the indexes that only generation needs, leaving the delta in the form that ships. + // Must be called outside of a transaction, as it vacuums to reclaim the freed pages. + void PrepareTablesForPackaging(SQLite::Connection& connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h index 60c0efc9e0..ea1995c703 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h @@ -30,9 +30,5 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 // Records the baseline time for this index, and generates a delta index against a previous // baseline when the caller has supplied the paths to do so. void CreateAdditionalPackagingOutput(const SQLiteIndexContext& context) override; - - // Records removals in the update tracking table rather than deleting the row, - // so that delta generation can see which packages have gone away. - V2_0::PackageUpdateTrackingTable::RemovalBehavior GetTrackingRemovalBehavior() const override; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp index b73e655c39..008819c103 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -11,7 +11,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 { - Interface::Interface(Utility::NormalizationVersion normVersion) : V2_0::Interface(normVersion) {} + Interface::Interface(Utility::NormalizationVersion normVersion) : V2_0::Interface(normVersion) + { + // Removals are recorded rather than deleted, so that delta generation can see which + // packages have gone away. This is the difference that 2.1 exists for. + m_trackingRemovalBehavior = V2_0::PackageUpdateTrackingTable::RemovalBehavior::Record; + } SQLite::Version Interface::GetVersion() const { @@ -84,8 +89,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 baselineTime = std::stoll(baselineTimeString.value()); } - auto changedPackages = V2_0::PackageUpdateTrackingTable::GetUpdatesSince(connection, baselineTime, GetTrackingRemovalBehavior()); - auto removedPackages = V2_0::PackageUpdateTrackingTable::GetRemovalsSince(connection, baselineTime, GetTrackingRemovalBehavior()); + auto changedPackages = V2_0::PackageUpdateTrackingTable::GetUpdatesSince(connection, baselineTime, m_trackingRemovalBehavior); + auto removedPackages = V2_0::PackageUpdateTrackingTable::GetRemovalsSince(connection, baselineTime, m_trackingRemovalBehavior); Delta::Generate( connection, @@ -95,9 +100,4 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 changedPackages, removedPackages); } - - V2_0::PackageUpdateTrackingTable::RemovalBehavior Interface::GetTrackingRemovalBehavior() const - { - return V2_0::PackageUpdateTrackingTable::RemovalBehavior::Record; - } } From cfbc85bc0f5b66bf4da306986542e4c802e55537 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Thu, 3 Sep 2026 16:32:07 -0700 Subject: [PATCH 23/36] Hook up SQLiteIndex --- src/AppInstallerCLITests/SQLiteIndex.cpp | 92 +++++++++++++++++++ .../Microsoft/SQLiteIndex.cpp | 24 +++-- .../Microsoft/SQLiteIndex.h | 5 + .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 10 ++ .../Microsoft/Schema/2_1/DeltaViews.cpp | 30 ++++++ .../Microsoft/Schema/2_1/Interface.h | 18 +++- .../Microsoft/Schema/2_1/Interface_2_1.cpp | 16 ++++ .../Microsoft/Schema/ISQLiteIndex.cpp | 10 ++ .../Microsoft/Schema/ISQLiteIndex.h | 11 +++ 9 files changed, 209 insertions(+), 7 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index b2ba3d1223..005211a494 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -4176,6 +4177,7 @@ TEST_CASE("SQLiteIndex_Delta_AddedPackage", "[sqliteindex][V2_1][delta]") std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); prepared.PrepareForPackaging(); + prepared.MarkAsBaseline(); } // Add a new package to the working index and generate a delta @@ -4230,6 +4232,7 @@ TEST_CASE("SQLiteIndex_Delta_RemovedPackage", "[sqliteindex][V2_1][delta]") std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); prepared.PrepareForPackaging(); + prepared.MarkAsBaseline(); } // Remove Publisher2 from the working index and generate a delta @@ -4278,6 +4281,7 @@ TEST_CASE("SQLiteIndex_Delta_NoChanges_EmptyDelta", "[sqliteindex][V2_1][delta]" std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); prepared.PrepareForPackaging(); + prepared.MarkAsBaseline(); } // Set tracking base to "now" so no packages appear changed @@ -4365,6 +4369,7 @@ TEST_CASE("SQLiteIndex_Delta_MergedViews_AssociationsAreSuppressedPerRow", "[sql std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); prepared.PrepareForPackaging(); + prepared.MarkAsBaseline(); } // Publisher1 trades t2 for t3, keeping t1. Publisher2 goes away entirely. @@ -4417,6 +4422,7 @@ TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_Search", "[sqliteindex][V2_1][delt std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); prepared.PrepareForPackaging(); + prepared.MarkAsBaseline(); } ManifestAndPath m2; @@ -4480,6 +4486,7 @@ TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_RemovedPackageExcluded", "[sqlitei std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); prepared.PrepareForPackaging(); + prepared.MarkAsBaseline(); } // Remove Publisher2 and generate delta @@ -4511,3 +4518,88 @@ TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_RemovedPackageExcluded", "[sqlitei REQUIRE(id.value() == "Publisher1.Id"); } +TEST_CASE("SQLiteIndex_Delta_UnmarkedBaselineRejected", "[sqliteindex][V2_1][delta]") +{ + TempFile workingFile{ "delta_working"s, ".db"s }; + TempFile baselineFile{ "delta_baseline"s, ".db"s }; + TempFile deltaFile{ "delta_output"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + + // Prepare a baseline but never designate it as one, so it has no identity for a delta to name. + { + SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + + std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + } + + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + index.AddManifest(m2.Manifest, m2.Path); + + index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); + index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); + + REQUIRE_THROWS_HR(index.PrepareForPackaging(), APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); +} + +TEST_CASE("SQLiteIndex_Delta_MismatchedBaselineRejected", "[sqliteindex][V2_1][delta]") +{ + TempFile workingFile{ "delta_working"s, ".db"s }; + TempFile baselineFile{ "delta_baseline"s, ".db"s }; + TempFile otherBaselineFile{ "delta_baseline_other"s, ".db"s }; + TempFile deltaFile{ "delta_output"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + + // Two baselines with identical contents. Designating each gives it its own identity, which is + // the point: a delta is tied to the baseline it was computed from, not to data that looks like it. + { + SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + + for (TempFile* file : { &baselineFile, &otherBaselineFile }) + { + std::filesystem::copy_file(workingFile.GetPath(), file->GetPath(), std::filesystem::copy_options::overwrite_existing); + SQLiteIndex prepared = SQLiteIndex::Open(file->GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + prepared.MarkAsBaseline(); + } + } + + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + { + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + index.AddManifest(m2.Manifest, m2.Path); + + index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); + index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); + index.PrepareForPackaging(); + } + + // The baseline it was generated against opens fine. + REQUIRE_NOTHROW(SQLiteIndex::OpenWithBaseline(deltaFile.GetPath().u8string(), baselineFile.GetPath().u8string())); + + REQUIRE_THROWS_HR( + SQLiteIndex::OpenWithBaseline(deltaFile.GetPath().u8string(), otherBaselineFile.GetPath().u8string()), + APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp index 2ab82c080c..325139b397 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -5,7 +5,6 @@ #include #include "ArpVersionValidation.h" #include -#include "Microsoft/Schema/2_0/Interface.h" namespace AppInstaller::Repository::Microsoft { @@ -56,11 +55,10 @@ namespace AppInstaller::Repository::Microsoft result.m_contextData.Add(baselinePath); - // TODO: Add a new interface function for this rather than casting - // The interface must be V2_0 to support delta read mode - //auto* v2Interface = dynamic_cast(result.m_interface.get()); - //THROW_HR_IF(E_NOTIMPL, v2Interface == nullptr); - //v2Interface->SetupDeltaReadMode(result.m_dbconn, baselinePath); + // The interface for the delta's schema version establishes the combined view. A version + // that does not understand deltas throws, which is the right answer: nothing else here + // could make sense of the pair. + result.m_interface->SetupDeltaReadMode(result.m_dbconn, baselineFilePath); return result; } @@ -277,6 +275,20 @@ namespace AppInstaller::Repository::Microsoft m_interface->PrepareForPackaging(Schema::SQLiteIndexContext{ m_dbconn, m_contextData }); } + void SQLiteIndex::MarkAsBaseline() + { + std::lock_guard lockInterface{ *m_interfaceLock }; + AICLI_LOG(Repo, Info, << "Marking index as a delta baseline"); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "sqliteindex_markasbaseline"); + + m_interface->MarkAsBaseline(m_dbconn); + + SetLastWriteTime(); + + savepoint.Commit(); + } + bool SQLiteIndex::CheckConsistency(bool log) const { std::lock_guard lockInterface{ *m_interfaceLock }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h index a6d07cd854..88f35608e4 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h @@ -126,6 +126,11 @@ namespace AppInstaller::Repository::Microsoft // Removes data that is no longer needed for an index that is to be published. void PrepareForPackaging(); + // Designates this index as a baseline that delta indexes may be generated against. + // Should be called on an index that has been prepared for packaging, as that is the form + // that will be published and that a delta will later be paired with. + void MarkAsBaseline(); + // Checks the consistency of the index to ensure that every referenced row exists. // Returns true if index is consistent; false if it is not. bool CheckConsistency(bool log = false) const; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp index 51b65fe3ed..788d737480 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "Microsoft/Schema/2_1/DeltaGeneration.h" #include "Microsoft/Schema/2_1/DeltaTables.h" +#include "Microsoft/Schema/2_1/Interface.h" #include "Microsoft/Schema/2_0/PackagesTable.h" #include "Microsoft/Schema/2_0/OneToManyTableWithMap.h" @@ -10,6 +11,7 @@ #include #include +#include #include #include @@ -305,11 +307,19 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta AICLI_LOG(Repo, Info, << "Generating delta index at [" << deltaOutputPath << "] for " << changedPackages.size() << " changed and " << removedPackages.size() << " removed packages"); + // A delta is only meaningful alongside the exact baseline it was computed from, so the + // baseline has to be one that was designated as such and can therefore be named. + std::optional baselineIdentifier = + SQLite::MetadataTable::TryGetNamedValue(baselineConnection, s_MetadataValueName_BaselineIdentifier); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED, !baselineIdentifier || baselineIdentifier->empty()); + DeltaDatabase deltaDatabase{ deltaOutputPath, version }; SQLite::Connection& deltaConnection = deltaDatabase.GetConnection(); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(deltaConnection, "delta_generate_v2_1"); + SQLite::MetadataTable::SetNamedValue(deltaConnection, s_MetadataValueName_DeltaBaselineIdentifier, baselineIdentifier.value()); + std::map nextValueRowIds; for (const auto& table : OneToManyTables()) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp index 414066a9df..695c3f3980 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp @@ -3,13 +3,16 @@ #include "pch.h" #include "Microsoft/Schema/2_1/DeltaViews.h" #include "Microsoft/Schema/2_1/DeltaTables.h" +#include "Microsoft/Schema/2_1/Interface.h" #include "Microsoft/Schema/2_0/PackagesTable.h" #include "Microsoft/Schema/2_0/OneToManyTableWithMap.h" #include "Microsoft/Schema/2_0/SystemReferenceStringTable.h" #include +#include +#include #include @@ -146,12 +149,39 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta builder.Execute(connection); } + + // Verifies that the baseline is the one that the delta was generated against. + // + // Merging a delta with any other baseline produces plausible looking nonsense rather than + // an error: the packages it did not change are taken from a version of the world it never + // saw, and the rowids that tie the two together mean different things on each side. + // + // The baseline is read on a connection of its own because the metadata accessors always + // read the main database, and by the time it is attached it is not that. + void ValidateBaselineAffinity(const SQLite::Connection& connection, const std::string& baselinePath) + { + std::optional expected = + SQLite::MetadataTable::TryGetNamedValue(connection, s_MetadataValueName_DeltaBaselineIdentifier); + + SQLite::Connection baselineConnection = SQLite::Connection::Create(baselinePath, SQLite::Connection::OpenDisposition::ReadOnly); + std::optional actual = + SQLite::MetadataTable::TryGetNamedValue(baselineConnection, s_MetadataValueName_BaselineIdentifier); + + if (!expected || !actual || expected.value() != actual.value()) + { + AICLI_LOG(Repo, Error, << "Delta expects baseline [" << expected.value_or("") << + "] but was given [" << actual.value_or("") << "]"); + THROW_HR(APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); + } + } } void SetupReadMode(SQLite::Connection& connection, const std::string& baselinePath) { AICLI_LOG(Repo, Info, << "Setting up delta read mode with baseline [" << baselinePath << "]"); + ValidateBaselineAffinity(connection, baselinePath); + { StatementBuilder builder; builder.Attach(baselinePath, s_Delta_BaselineSchema); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h index ea1995c703..a8736812ab 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h @@ -8,6 +8,17 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 // The point in time from which the next delta generated against this index should be computed. static constexpr std::string_view s_MetadataValueName_DeltaBaselineTime = "deltaBaselineTime"sv; + // Identifies this index as a baseline that deltas may be generated against. + // + // The database identifier cannot serve this purpose. An index is prepared from a copy of a + // long lived working index, and copying carries the identifier along, so every index produced + // in a baseline period shares one. Designation therefore stamps its own fresh identity. + static constexpr std::string_view s_MetadataValueName_BaselineIdentifier = "baselineIdentifier"sv; + + // Written into a delta, naming the baseline that it was generated against. A delta is only + // meaningful when paired with that exact baseline, so this is checked when the two are opened. + static constexpr std::string_view s_MetadataValueName_DeltaBaselineIdentifier = "deltaBaselineIdentifier"sv; + // Interface to schema version 2.1 exposed through ISQLiteIndex. // Version 2.1 adds the is_removed column to the update_tracking table, // enabling delta index generation that can represent package removals. @@ -21,10 +32,15 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 // Version 2.0 bool MigrateFrom(SQLite::Connection& connection, const ISQLiteIndex* current) override; + // Version 2.1 + + // Designates this index as a baseline that deltas may be generated against. + void MarkAsBaseline(SQLite::Connection& connection) override; + // Sets this index up to read the combination of a delta and the baseline it was generated // against. Attaches the baseline and defines the merged views, after which every inherited // read path operates on the combination. Must be called before any read. - void SetupDeltaReadMode(SQLite::Connection& connection, const std::string& baselinePath); + void SetupDeltaReadMode(SQLite::Connection& connection, const std::string& baselinePath) override; protected: // Records the baseline time for this index, and generates a delta index against a previous diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp index 008819c103..4db607e0c0 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -9,6 +9,8 @@ #include #include +#include + namespace AppInstaller::Repository::Microsoft::Schema::V2_1 { Interface::Interface(Utility::NormalizationVersion normVersion) : V2_0::Interface(normVersion) @@ -46,6 +48,20 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 return false; } + void Interface::MarkAsBaseline(SQLite::Connection& connection) + { + GUID baselineIdentifier; + THROW_IF_FAILED(CoCreateGuid(&baselineIdentifier)); + + std::ostringstream stream; + stream << baselineIdentifier; + std::string value = stream.str(); + + AICLI_LOG(Repo, Info, << "Marking index as a delta baseline with identifier [" << value << "]"); + + SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_BaselineIdentifier, value); + } + void Interface::SetupDeltaReadMode(SQLite::Connection& connection, const std::string& baselinePath) { Delta::SetupReadMode(connection, baselinePath); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp index ed821f9d2f..a8d05ecd82 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp @@ -26,6 +26,16 @@ namespace AppInstaller::Repository::Microsoft::Schema THROW_WIN32(ERROR_NOT_SUPPORTED); } + void ISQLiteIndex::MarkAsBaseline(SQLite::Connection&) + { + THROW_WIN32(ERROR_NOT_SUPPORTED); + } + + void ISQLiteIndex::SetupDeltaReadMode(SQLite::Connection&, const std::string&) + { + THROW_WIN32(ERROR_NOT_SUPPORTED); + } + std::unique_ptr CreateISQLiteIndex(const SQLite::Version& version) { if (version.MajorVersion == 1 || diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h index 12de8e786f..91418552ed 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h @@ -151,6 +151,17 @@ namespace AppInstaller::Repository::Microsoft::Schema // Set the property value. virtual void SetProperty(SQLite::Connection& connection, Property property, const std::string& value); + + // Version 2.1 + + // Designates this index as a baseline that delta indexes may be generated against, giving + // it an identity that a delta can name. A schema version that cannot be a baseline throws. + virtual void MarkAsBaseline(SQLite::Connection& connection); + + // Sets this index up to read the combination of a delta and the baseline that it was + // generated against, so that every subsequent read sees the merged data. Must be called + // before any read. A schema version that cannot read a delta throws. + virtual void SetupDeltaReadMode(SQLite::Connection& connection, const std::string& baselinePath); }; DEFINE_ENUM_FLAG_OPERATORS(ISQLiteIndex::CreateOptions); From aa38241d714129a1d2e48830739ea1c5ad7f9101 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Thu, 3 Sep 2026 17:55:42 -0700 Subject: [PATCH 24/36] Add DatabaseSpecifier --- src/AppInstallerCLITests/SQLiteIndex.cpp | 10 ++- src/AppInstallerCLITests/SQLiteWrapper.cpp | 46 ++++++++++- .../Microsoft/SQLiteIndex.cpp | 22 +++-- .../Microsoft/SQLiteIndex.h | 5 +- .../Microsoft/Schema/2_1/DeltaTables.h | 8 +- .../Microsoft/Schema/2_1/DeltaViews.cpp | 12 +-- .../Microsoft/Schema/2_1/DeltaViews.h | 2 +- .../Microsoft/Schema/2_1/Interface.h | 2 +- .../Microsoft/Schema/2_1/Interface_2_1.cpp | 4 +- .../Microsoft/Schema/ISQLiteIndex.cpp | 2 +- .../Microsoft/Schema/ISQLiteIndex.h | 2 +- .../Public/winget/SQLiteStatementBuilder.h | 6 +- .../Public/winget/SQLiteStorageBase.h | 12 +-- .../Public/winget/SQLiteWrapper.h | 44 ++++++++++ .../SQLiteStatementBuilder.cpp | 4 +- .../SQLiteStorageBase.cpp | 67 ++------------- src/AppInstallerSharedLib/SQLiteWrapper.cpp | 81 +++++++++++++++++++ 17 files changed, 227 insertions(+), 102 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index 005211a494..dd9f0b0b22 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -4392,7 +4392,7 @@ TEST_CASE("SQLiteIndex_Delta_MergedViews_AssociationsAreSuppressedPerRow", "[sql REQUIRE(std::filesystem::exists(deltaFile.GetPath())); Connection deltaConnection = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); - Schema::V2_1::Delta::SetupReadMode(deltaConnection, baselineFile.GetPath().u8string()); + Schema::V2_1::Delta::SetupReadMode(deltaConnection, DatabaseSpecifier{ baselineFile.GetPath().u8string(), DatabaseDisposition::Read }); // The delta records only what changed about Publisher1, so it never mentions t1 at all. // Suppressing the baseline at the level of the package would therefore lose it. @@ -4405,6 +4405,11 @@ TEST_CASE("SQLiteIndex_Delta_MergedViews_AssociationsAreSuppressedPerRow", "[sql TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_Search", "[sqliteindex][V2_1][delta]") { + // Immutable is the disposition the shipped path uses, and it is the one that reaches SQLite as + // a URI rather than a plain path, so both files have to be named that way for the attach to + // resolve at all. + auto disposition = GENERATE(SQLiteStorageBase::OpenDisposition::Read, SQLiteStorageBase::OpenDisposition::Immutable); + TempFile workingFile{ "delta_working"s, ".db"s }; TempFile baselineFile{ "delta_baseline"s, ".db"s }; TempFile workingFile2{ "delta_working2"s, ".db"s }; @@ -4447,7 +4452,8 @@ TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_Search", "[sqliteindex][V2_1][delt // Open the delta combined with the baseline SQLiteIndex combined = SQLiteIndex::OpenWithBaseline( deltaFile.GetPath().u8string(), - baselineFile.GetPath().u8string()); + baselineFile.GetPath().u8string(), + disposition); // Search should return both Publisher1 (from baseline) and Publisher2 (from delta) auto results = combined.Search({}); diff --git a/src/AppInstallerCLITests/SQLiteWrapper.cpp b/src/AppInstallerCLITests/SQLiteWrapper.cpp index f7a299ff26..50cf907909 100644 --- a/src/AppInstallerCLITests/SQLiteWrapper.cpp +++ b/src/AppInstallerCLITests/SQLiteWrapper.cpp @@ -931,7 +931,7 @@ TEST_CASE("SQLBuilder_AttachAndTempView", "[sqlbuilder]") { INFO("Attach the baseline database"); Builder::StatementBuilder attach; - attach.Attach(baselineFile.GetPath().u8string(), baselineAlias); + attach.Attach(DatabaseSpecifier{ baselineFile.GetPath().u8string(), DatabaseDisposition::Read }, baselineAlias); attach.Execute(connection); } @@ -1158,3 +1158,47 @@ TEST_CASE("SQLiteWrapperTransactionWriteConflict", "[sqlitewrapper]") SelectFromSimpleTestTableOnlyOneRow(connection, firstVal, secondVal); } + +TEST_CASE("SQLiteDatabaseSpecifierTargets", "[sqlitewrapper]") +{ + // A disposition that SQLite can express with flags alone hands over the path untouched. + DatabaseSpecifier plain{ "D:\\test\\index.db"s, DatabaseDisposition::Read }; + REQUIRE(plain.Target() == "D:\\test\\index.db"); + REQUIRE(plain.ConnectionDisposition() == Connection::OpenDisposition::ReadOnly); + + // Immutability can only be asked for through a URI query parameter. + DatabaseSpecifier immutable{ "D:\\test\\index.db"s, DatabaseDisposition::Immutable }; + REQUIRE(immutable.Target() == "file:/D:/test/index.db?immutable=1"); + REQUIRE(immutable.ConnectionDisposition() == Connection::OpenDisposition::ReadOnly); + + // Characters that would otherwise start the query or fragment are escaped, and repeated + // separators collapse, per the conversion the URI documentation prescribes. + DatabaseSpecifier escaped{ "D:\\a#b\\\\c?d\\index.db"s, DatabaseDisposition::Immutable }; + REQUIRE(escaped.Target() == "file:/D:/a%23b/c%3fd/index.db?immutable=1"); + + // Every disposition enables URI handling, because a connection that did not ask for it cannot + // attach one later regardless of how the attached database is named. + REQUIRE(plain.ConnectionFlags() == Connection::OpenFlags::Uri); + REQUIRE(immutable.ConnectionFlags() == Connection::OpenFlags::Uri); +} + +TEST_CASE("SQLiteDatabaseSpecifierImmutableOpen", "[sqlitewrapper]") +{ + TestCommon::TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + int firstVal = 1; + std::string secondVal = "test"; + + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::Create); + + CreateSimpleTestTable(connection); + + InsertIntoSimpleTestTable(connection, firstVal, secondVal); + } + + Connection connection = Connection::Create(DatabaseSpecifier{ tempFile.GetPath().u8string(), DatabaseDisposition::Immutable }); + + SelectFromSimpleTestTableOnlyOneRow(connection, firstVal, secondVal); +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp index 325139b397..30aca55a70 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -45,20 +45,25 @@ namespace AppInstaller::Repository::Microsoft return { filePath, source }; } - SQLiteIndex SQLiteIndex::OpenWithBaseline(const std::string& deltaFilePath, const std::string& baselineFilePath) + SQLiteIndex SQLiteIndex::OpenWithBaseline(const std::string& deltaFilePath, const std::string& baselineFilePath, OpenDisposition disposition) { AICLI_LOG(Repo, Info, << "Opening delta index [" << deltaFilePath << "] with baseline [" << baselineFilePath << "]"); - SQLiteIndex result{ deltaFilePath, SQLiteStorageBase::OpenDisposition::Read, {} }; + + // The combined form is presented through views over a union of two databases, so there is + // nothing here that could be written back to. + THROW_HR_IF(E_INVALIDARG, disposition == OpenDisposition::ReadWrite); std::filesystem::path baselinePath{ Utility::ConvertToUTF16(baselineFilePath) }; THROW_HR_IF(E_INVALIDARG, baselinePath.empty() || baselinePath.is_relative()); + SQLiteIndex result{ SQLite::DatabaseSpecifier{ deltaFilePath, disposition }, {} }; + result.m_contextData.Add(baselinePath); // The interface for the delta's schema version establishes the combined view. A version // that does not understand deltas throws, which is the right answer: nothing else here // could make sense of the pair. - result.m_interface->SetupDeltaReadMode(result.m_dbconn, baselineFilePath); + result.m_interface->SetupDeltaReadMode(result.m_dbconn, SQLite::DatabaseSpecifier{ baselineFilePath, disposition }); return result; } @@ -72,13 +77,18 @@ namespace AppInstaller::Repository::Microsoft } SQLiteIndex::SQLiteIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile) : - SQLiteStorageBase(target, disposition, std::move(indexFile)) + SQLiteIndex(SQLite::DatabaseSpecifier{ target, disposition }, std::move(indexFile)) + { + } + + SQLiteIndex::SQLiteIndex(const SQLite::DatabaseSpecifier& specifier, Utility::ManagedFile&& indexFile) : + SQLiteStorageBase(specifier, std::move(indexFile)) { m_dbconn.EnableICU(); AICLI_LOG(Repo, Info, << "Opened SQLite Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]"); m_interface = Schema::CreateISQLiteIndex(m_version); - THROW_HR_IF(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX, disposition == SQLiteStorageBase::OpenDisposition::ReadWrite && m_version != m_interface->GetVersion()); - SetDatabaseFilePath(target); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX, specifier.Disposition() == SQLiteStorageBase::OpenDisposition::ReadWrite && m_version != m_interface->GetVersion()); + SetDatabaseFilePath(specifier.Path()); } SQLiteIndex::SQLiteIndex(const std::string& target, SQLiteIndex& source) : diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h index 88f35608e4..aa90e762a2 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h @@ -58,7 +58,8 @@ namespace AppInstaller::Repository::Microsoft // Opens a delta index combined with its baseline for reading. // The delta is the main connection; the baseline is ATTACHed and TEMP VIEWs are created // so that existing search code operates transparently across both. - static SQLiteIndex OpenWithBaseline(const std::string& deltaFilePath, const std::string& baselineFilePath); + // The disposition applies to both files, because the pair is only meaningful as a unit. + static SQLiteIndex OpenWithBaseline(const std::string& deltaFilePath, const std::string& baselineFilePath, OpenDisposition disposition = OpenDisposition::Read); #ifndef AICLI_DISABLE_TEST_HOOKS // Changes the version of the interface being used to operate on the database. @@ -192,6 +193,8 @@ namespace AppInstaller::Repository::Microsoft // Constructor used to open an existing index. SQLiteIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile); + SQLiteIndex(const SQLite::DatabaseSpecifier& specifier, Utility::ManagedFile&& indexFile); + // Constructor used to copy the given index. SQLiteIndex(const std::string& target, SQLiteIndex& source); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h index c26a9e7726..6376652d83 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h @@ -17,10 +17,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta }; // The tables that store a value directly alongside the package that it refers to. - std::vector SystemReferenceTables(); + const std::vector& SystemReferenceTables(); // The tables that store values in a data table, associated with packages through a map table. - std::vector OneToManyTables(); + const std::vector& OneToManyTables(); // Gets the name of the delta table that mirrors the given 2.0 table. // The delta tables are named distinctly so that a delta database can be attached alongside @@ -35,8 +35,4 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta // Creates the full set of delta tables in the given database. void CreateTables(SQLite::Connection& connection); - - // Drops the indexes that only generation needs, leaving the delta in the form that ships. - // Must be called outside of a transaction, as it vacuums to reclaim the freed pages. - void PrepareTablesForPackaging(SQLite::Connection& connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp index 695c3f3980..71fb64f7a8 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp @@ -158,12 +158,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta // // The baseline is read on a connection of its own because the metadata accessors always // read the main database, and by the time it is attached it is not that. - void ValidateBaselineAffinity(const SQLite::Connection& connection, const std::string& baselinePath) + void ValidateBaselineAffinity(const SQLite::Connection& connection, const SQLite::DatabaseSpecifier& baseline) { std::optional expected = SQLite::MetadataTable::TryGetNamedValue(connection, s_MetadataValueName_DeltaBaselineIdentifier); - SQLite::Connection baselineConnection = SQLite::Connection::Create(baselinePath, SQLite::Connection::OpenDisposition::ReadOnly); + SQLite::Connection baselineConnection = SQLite::Connection::Create(baseline); std::optional actual = SQLite::MetadataTable::TryGetNamedValue(baselineConnection, s_MetadataValueName_BaselineIdentifier); @@ -176,15 +176,15 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta } } - void SetupReadMode(SQLite::Connection& connection, const std::string& baselinePath) + void SetupReadMode(SQLite::Connection& connection, const SQLite::DatabaseSpecifier& baseline) { - AICLI_LOG(Repo, Info, << "Setting up delta read mode with baseline [" << baselinePath << "]"); + AICLI_LOG(Repo, Info, << "Setting up delta read mode with baseline [" << baseline.Path() << "]"); - ValidateBaselineAffinity(connection, baselinePath); + ValidateBaselineAffinity(connection, baseline); { StatementBuilder builder; - builder.Attach(baselinePath, s_Delta_BaselineSchema); + builder.Attach(baseline, s_Delta_BaselineSchema); builder.Execute(connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h index 0a570234bf..015d6afa50 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h @@ -14,5 +14,5 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta // The views take the 2.0 table names for themselves, and the delta's own tables are named // distinctly, so every 2.0 read path operates on the merged data without knowing that it is // merged. The views are temporary, so they last only as long as the connection. - void SetupReadMode(SQLite::Connection& connection, const std::string& baselinePath); + void SetupReadMode(SQLite::Connection& connection, const SQLite::DatabaseSpecifier& baseline); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h index a8736812ab..472fd7061c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h @@ -40,7 +40,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 // Sets this index up to read the combination of a delta and the baseline it was generated // against. Attaches the baseline and defines the merged views, after which every inherited // read path operates on the combination. Must be called before any read. - void SetupDeltaReadMode(SQLite::Connection& connection, const std::string& baselinePath) override; + void SetupDeltaReadMode(SQLite::Connection& connection, const SQLite::DatabaseSpecifier& baseline) override; protected: // Records the baseline time for this index, and generates a delta index against a previous diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp index 4db607e0c0..54a014d6ba 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -62,9 +62,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_BaselineIdentifier, value); } - void Interface::SetupDeltaReadMode(SQLite::Connection& connection, const std::string& baselinePath) + void Interface::SetupDeltaReadMode(SQLite::Connection& connection, const SQLite::DatabaseSpecifier& baseline) { - Delta::SetupReadMode(connection, baselinePath); + Delta::SetupReadMode(connection, baseline); // The merged data is presented through views rather than tables, so the checks that the // base makes to decide whether this index has been packaged cannot see it. Record that the diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp index a8d05ecd82..890317e8ce 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp @@ -31,7 +31,7 @@ namespace AppInstaller::Repository::Microsoft::Schema THROW_WIN32(ERROR_NOT_SUPPORTED); } - void ISQLiteIndex::SetupDeltaReadMode(SQLite::Connection&, const std::string&) + void ISQLiteIndex::SetupDeltaReadMode(SQLite::Connection&, const SQLite::DatabaseSpecifier&) { THROW_WIN32(ERROR_NOT_SUPPORTED); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h index 91418552ed..9c2eebc92c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h @@ -161,7 +161,7 @@ namespace AppInstaller::Repository::Microsoft::Schema // Sets this index up to read the combination of a delta and the baseline that it was // generated against, so that every subsequent read sees the merged data. Must be called // before any read. A schema version that cannot read a delta throws. - virtual void SetupDeltaReadMode(SQLite::Connection& connection, const std::string& baselinePath); + virtual void SetupDeltaReadMode(SQLite::Connection& connection, const SQLite::DatabaseSpecifier& baseline); }; DEFINE_ENUM_FLAG_OPERATORS(ISQLiteIndex::CreateOptions); diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h index 0f791ceb64..f3b6e722a9 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h @@ -519,11 +519,11 @@ namespace AppInstaller::SQLite::Builder // Output the set portion of an update statement. StatementBuilder& Vacuum(); - // Attaches another database file to the connection under the given alias. - // The file path is bound as a parameter rather than embedded in the statement text. + // Attaches another database to the connection under the given alias. + // The target is bound as a parameter rather than embedded in the statement text. // The alias remains valid only for the connection that executes the statement, and // the attachment is released when that connection is closed. - StatementBuilder& Attach(const std::string& path, std::string_view alias); + StatementBuilder& Attach(const DatabaseSpecifier& specifier, std::string_view alias); // General purpose functions to begin and end a parenthetical expression. StatementBuilder& BeginParenthetical(); diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h index 8b4958343d..4b8a659d2d 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h @@ -14,15 +14,7 @@ namespace AppInstaller::SQLite struct SQLiteStorageBase { // The disposition for opening the database. - enum class OpenDisposition - { - // Open for read only. - Read, - // Open for read and write. - ReadWrite, - // The database will not change while in use; open for immutable read. - Immutable, - }; + using OpenDisposition = DatabaseDisposition; // Gets the last write time for the database. std::chrono::system_clock::time_point GetLastWriteTime() const; @@ -43,6 +35,8 @@ namespace AppInstaller::SQLite SQLiteStorageBase(const std::string& filePath, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile); + SQLiteStorageBase(const DatabaseSpecifier& specifier, Utility::ManagedFile&& indexFile); + SQLiteStorageBase(const std::string& target, SQLiteStorageBase& source); // Sets the last write time metadata value in the database. diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h b/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h index 0f100b4394..50d355e488 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h @@ -206,6 +206,7 @@ namespace AppInstaller::SQLite }; struct Statement; + struct DatabaseSpecifier; // The connection to a database. struct Connection @@ -234,6 +235,9 @@ namespace AppInstaller::SQLite static Connection Create(const std::string& target, OpenDisposition disposition, OpenFlags flags = OpenFlags::None); + // Creates a connection to the database that the specifier identifies. + static Connection Create(const DatabaseSpecifier& specifier); + Connection() = default; Connection(const Connection&) = delete; @@ -282,6 +286,46 @@ namespace AppInstaller::SQLite std::shared_ptr m_dbconn; }; + // How a database is intended to be used. + enum class DatabaseDisposition + { + // Open for read only. + Read, + // Open for read and write. + ReadWrite, + // The database will not change while in use; open for immutable read. + Immutable, + }; + + // Identifies a database and how it is to be used, translating that into the target string and + // flags that SQLite requires. + // + // The translation cannot live inside the act of opening a connection, because `ATTACH` takes + // the same target string as `sqlite3_open_v2` and has to be given the identical value. Anything + // that needs to name a database therefore takes one of these rather than a bare path. + struct DatabaseSpecifier + { + DatabaseSpecifier(std::string path, DatabaseDisposition disposition); + + // The path to the database file, as given. + const std::string& Path() const { return m_path; } + + DatabaseDisposition Disposition() const { return m_disposition; } + + // The value to hand to SQLite, which is a URI when the disposition requires query + // parameters to express it and the path itself otherwise. + const std::string& Target() const { return m_target; } + + // The connection level disposition and flags that carry this disposition. + Connection::OpenDisposition ConnectionDisposition() const; + Connection::OpenFlags ConnectionFlags() const; + + private: + std::string m_path; + std::string m_target; + DatabaseDisposition m_disposition; + }; + // A SQL statement. struct Statement { diff --git a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp index 19692e2b1d..22ed296422 100644 --- a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp +++ b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp @@ -972,10 +972,10 @@ namespace AppInstaller::SQLite::Builder return *this; } - StatementBuilder& StatementBuilder::Attach(const std::string& path, std::string_view alias) + StatementBuilder& StatementBuilder::Attach(const DatabaseSpecifier& specifier, std::string_view alias) { m_stream << "ATTACH DATABASE ?"; - AddBindFunctor(m_bindIndex++, path); + AddBindFunctor(m_bindIndex++, specifier.Target()); OutputOperationAndTable(m_stream, " AS", alias); return *this; } diff --git a/src/AppInstallerSharedLib/SQLiteStorageBase.cpp b/src/AppInstallerSharedLib/SQLiteStorageBase.cpp index 61d50dfb78..dff989519f 100644 --- a/src/AppInstallerSharedLib/SQLiteStorageBase.cpp +++ b/src/AppInstallerSharedLib/SQLiteStorageBase.cpp @@ -39,8 +39,6 @@ namespace AppInstaller::SQLite } } - // One method for converting open disposition to proper open disposition - // another method for obtaining the right flags void SQLiteStorageBase::SetLastWriteTime() { MetadataTable::SetNamedValue(m_dbconn, s_MetadataValueName_LastWriteTime, Utility::GetCurrentUnixEpoch()); @@ -91,67 +89,16 @@ namespace AppInstaller::SQLite } SQLiteStorageBase::SQLiteStorageBase(const std::string& filePath, OpenDisposition disposition, Utility::ManagedFile&& file) : - m_indexFile(std::move(file)) + SQLiteStorageBase(DatabaseSpecifier{ filePath, disposition }, std::move(file)) { - AICLI_LOG(Repo, Info, << "Opening database for " << GetOpenDispositionString(disposition) << " at '" << filePath << "'"); - switch (disposition) - { - case OpenDisposition::Read: - m_dbconn = SQLite::Connection::Create(filePath, SQLite::Connection::OpenDisposition::ReadOnly, SQLite::Connection::OpenFlags::None); - break; - case OpenDisposition::ReadWrite: - m_dbconn = SQLite::Connection::Create(filePath, SQLite::Connection::OpenDisposition::ReadWrite, SQLite::Connection::OpenFlags::None); - break; - case OpenDisposition::Immutable: - { - // Following the algorithm set forth at https://sqlite.org/uri.html [3.1] to convert to a URI path - // The execution order builds out the string so that it shouldn't require any moves (other than growing) - std::string target; - // Add an 'arbitrary' growth size to prevent the majority of needing to grow (adding 'file:/' and '?immutable=1') - target.reserve(filePath.size() + 20); - - target += "file:"; - - bool wasLastCharSlash = false; - - if (filePath.size() >= 2 && filePath[1] == ':' && - ((filePath[0] >= 'a' && filePath[0] <= 'z') || - (filePath[0] >= 'A' && filePath[0] <= 'Z'))) - { - target += '/'; - wasLastCharSlash = true; - } - - for (char c : filePath) - { - bool wasThisCharSlash = false; - switch (c) - { - case '?': target += "%3f"; break; - case '#': target += "%23"; break; - case '\\': - case '/': - { - wasThisCharSlash = true; - if (!wasLastCharSlash) - { - target += '/'; - } - break; - } - default: target += c; break; - } + } - wasLastCharSlash = wasThisCharSlash; - } + SQLiteStorageBase::SQLiteStorageBase(const DatabaseSpecifier& specifier, Utility::ManagedFile&& file) : + m_indexFile(std::move(file)) + { + AICLI_LOG(Repo, Info, << "Opening database for " << GetOpenDispositionString(specifier.Disposition()) << " at '" << specifier.Path() << "'"); - target += "?immutable=1"; - m_dbconn = SQLite::Connection::Create(filePath, SQLite::Connection::OpenDisposition::ReadOnly, SQLite::Connection::OpenFlags::Uri); - break; - } - default: - THROW_HR(E_UNEXPECTED); - } + m_dbconn = SQLite::Connection::Create(specifier); m_version = Version::GetSchemaVersion(m_dbconn); } diff --git a/src/AppInstallerSharedLib/SQLiteWrapper.cpp b/src/AppInstallerSharedLib/SQLiteWrapper.cpp index 78df7a5b9c..5e00ef872d 100644 --- a/src/AppInstallerSharedLib/SQLiteWrapper.cpp +++ b/src/AppInstallerSharedLib/SQLiteWrapper.cpp @@ -213,6 +213,11 @@ namespace AppInstaller::SQLite return result; } + Connection Connection::Create(const DatabaseSpecifier& specifier) + { + return Create(specifier.Target(), specifier.ConnectionDisposition(), specifier.ConnectionFlags()); + } + void Connection::EnableICU() { AICLI_LOG(SQL, Verbose, << "Enabling ICU"); @@ -265,6 +270,82 @@ namespace AppInstaller::SQLite return m_dbconn; } + DatabaseSpecifier::DatabaseSpecifier(std::string path, DatabaseDisposition disposition) : + m_path(std::move(path)), m_disposition(disposition) + { + if (m_disposition != DatabaseDisposition::Immutable) + { + m_target = m_path; + return; + } + + // Following the algorithm set forth at https://sqlite.org/uri.html [3.1] to convert to a URI path. + // The execution order builds out the string so that it shouldn't require any moves (other than growing). + // Add an 'arbitrary' growth size to prevent the majority of needing to grow (adding 'file:/' and '?immutable=1'). + m_target.reserve(m_path.size() + 20); + + m_target += "file:"; + + bool wasLastCharSlash = false; + + if (m_path.size() >= 2 && m_path[1] == ':' && + ((m_path[0] >= 'a' && m_path[0] <= 'z') || + (m_path[0] >= 'A' && m_path[0] <= 'Z'))) + { + m_target += '/'; + wasLastCharSlash = true; + } + + for (char c : m_path) + { + bool wasThisCharSlash = false; + switch (c) + { + case '?': m_target += "%3f"; break; + case '#': m_target += "%23"; break; + case '\\': + case '/': + { + wasThisCharSlash = true; + if (!wasLastCharSlash) + { + m_target += '/'; + } + break; + } + default: m_target += c; break; + } + + wasLastCharSlash = wasThisCharSlash; + } + + m_target += "?immutable=1"; + } + + Connection::OpenDisposition DatabaseSpecifier::ConnectionDisposition() const + { + switch (m_disposition) + { + case DatabaseDisposition::Read: + case DatabaseDisposition::Immutable: + return Connection::OpenDisposition::ReadOnly; + case DatabaseDisposition::ReadWrite: + return Connection::OpenDisposition::ReadWrite; + default: + THROW_HR(E_UNEXPECTED); + } + } + + Connection::OpenFlags DatabaseSpecifier::ConnectionFlags() const + { + // URI handling is enabled unconditionally, not only for the dispositions that produce one. + // SQLite decides whether a name is a URI when the connection is opened and applies that + // same decision to every later `ATTACH`, so a connection that did not ask for URIs cannot + // attach one. A name that does not begin with "file:" is always taken literally, so this + // costs nothing for the dispositions that hand over a plain path. + return Connection::OpenFlags::Uri; + } + Statement::Statement(const Connection& connection, std::string_view sql) { m_dbconn = connection.GetSharedConnection(); From 6942abc8bd04eafda6442594a938a81f2059a42d Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 4 Sep 2026 10:43:43 -0700 Subject: [PATCH 25/36] Fix some id consistency issues --- .../Microsoft/Schema/2_0/Interface_2_0.cpp | 2 +- .../Schema/2_0/PackageUpdateTrackingTable.cpp | 255 ++++++++++++++++-- .../Schema/2_0/PackageUpdateTrackingTable.h | 14 +- .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 51 +++- .../Microsoft/Schema/2_1/Interface_2_1.cpp | 2 +- 5 files changed, 294 insertions(+), 30 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index c0a2b8c921..d54f027c39 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -726,7 +726,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 SQLite::rowid_t packageId = PackagesTable::InsertWithRowId(connection, idRowId.value(), packageData); - PackagesTable::UpdateValueIdById(connection, packageId, PackageUpdateTrackingTable::GetDataHash(connection, packageIdentifier)); + PackagesTable::UpdateValueIdById(connection, packageId, PackageUpdateTrackingTable::GetDataHash(connection, packageIdentifier, m_trackingRemovalBehavior)); for (const auto& versionKey : versionKeys) { diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp index 56e9f33216..f23abfbff1 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp @@ -2,8 +2,16 @@ // Licensed under the MIT License. #include "pch.h" #include "PackageUpdateTrackingTable.h" +#include "Microsoft/Schema/1_0/IdTable.h" #include #include +#include + +#include +#include +#include +#include +#include using namespace AppInstaller::SQLite; @@ -12,11 +20,41 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 using namespace std::string_view_literals; static constexpr std::string_view s_PUTT_Table_Name = "update_tracking"sv; static constexpr std::string_view s_PUTT_WriteTimeIndex_Name = "update_tracking_write_idx"sv; + static constexpr std::string_view s_PUTT_LiveIndex_Name = "update_tracking_live_idx"sv; static constexpr std::string_view s_PUTT_Package = "package"sv; static constexpr std::string_view s_PUTT_WriteTime = "write_time"sv; static constexpr std::string_view s_PUTT_Manifest = "manifest"sv; static constexpr std::string_view s_PUTT_Hash = "hash"sv; static constexpr std::string_view s_PUTT_IsRemoved = "is_removed"sv; + static constexpr std::string_view s_PUTT_PackageRowId = "package_rowid"sv; + + namespace + { + // Finds the rowid that the package occupies in the index. + // + // The identity of a package is the rowid of its `ids` row, not the identifier string. + // AddManifest calls IdTable::EnsureExists with overwriteLikeMatch, so identifiers that + // match under LIKE collapse onto a single rowid and the stored string is replaced by the + // most recent casing. Matching by rowid therefore inherits the index's own notion of + // identity, and is stable against that string changing underneath us. + std::optional GetPackageRowIdInIndex(const SQLite::Connection& connection, const std::string& packageIdentifier) + { + return V1_0::IdTable::SelectIdByValue(connection, packageIdentifier, true); + } + + // Determines whether the package currently has a row that is not marked as removed. + bool HasLiveRow(const SQLite::Connection& connection, const std::string& packageIdentifier) + { + Builder::StatementBuilder builder; + builder.Select(Builder::RowCount).From(s_PUTT_Table_Name). + Where(s_PUTT_Package).LikeWithEscape(packageIdentifier). + And(s_PUTT_IsRemoved).Equals(0); + + Statement statement = builder.Prepare(connection); + THROW_HR_IF(E_UNEXPECTED, !statement.Step()); + return statement.GetColumn(0) != 0; + } + } std::string_view PackageUpdateTrackingTable::TableName() { @@ -39,6 +77,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (removals == RemovalBehavior::Record) { builder.Column(ColumnBuilder(s_PUTT_IsRemoved, Type::Int64).NotNull().Default(0)); + + // The rowid the package occupies in the index, which is what a delta is keyed on. + // 0 means unknown, which only arises for a removal of a package that was never + // tracked as present; generation resolves removals against the baseline anyway. + builder.Column(ColumnBuilder(s_PUTT_PackageRowId, Type::Int64).NotNull().Default(0)); } builder.EndColumns(); @@ -48,6 +91,29 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 StatementBuilder indexBuilder; indexBuilder.CreateIndex(s_PUTT_WriteTimeIndex_Name).On(s_PUTT_Table_Name).Columns(s_PUTT_WriteTime); indexBuilder.Execute(connection); + + if (removals == RemovalBehavior::Record) + { + CreateLiveRowIndex(connection); + } + } + + void PackageUpdateTrackingTable::CreateLiveRowIndex(SQLite::Connection& connection) + { + // A package occupies exactly one rowid at a time, so at most one row per rowid can be + // live. Tombstones are excluded because a rowid vacated by one package can be taken by + // another, leaving the old package's tombstone and the new package's live row sharing it. + // + // The constraint is deliberately on the rowid rather than the identifier: a unique index + // cannot use LIKE, and no available collation matches it. NOCASE is ASCII-only, while + // LIKE here is the ICU implementation registered by sqlite3IcuInit, so NOCASE would + // disagree with every other accessor on non-ASCII identifiers. An ICU collation cannot be + // used either, since it would bake the ICU version into a published index file and make + // the table unreadable to any connection that had not registered it. + Builder::StatementBuilder builder; + builder.CreateUniqueIndex(s_PUTT_LiveIndex_Name).On(s_PUTT_Table_Name).Columns(s_PUTT_PackageRowId). + Where(s_PUTT_IsRemoved).EqualsLiteral(0); + builder.Execute(connection); } void PackageUpdateTrackingTable::EnsureExists(SQLite::Connection& connection, RemovalBehavior removals) @@ -101,6 +167,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 else { // Mark the package as removed rather than deleting the row, clearing the data columns. + // Only the live row is marked; earlier tombstones refer to rowids the package has + // already vacated and must be preserved so that a delta learns about each of them. + // The package rowid is carried forward untouched, since the package is already gone + // from the index and can no longer be looked up there. int64_t currentTime = Utility::GetCurrentUnixEpoch(); Builder::StatementBuilder updateBuilder; @@ -109,14 +179,17 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 Column(s_PUTT_Manifest).AssignValue(nullptr). Column(s_PUTT_Hash).AssignValue(nullptr). Column(s_PUTT_IsRemoved).Equals(1). - Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); + Where(s_PUTT_Package).LikeWithEscape(packageIdentifier). + And(s_PUTT_IsRemoved).Equals(0); updateBuilder.Execute(connection); if (connection.GetChanges() == 0) { // The package was added and removed without an intervening tracking checkpoint, // so there is no row to mark. Record the removal so that a delta built against an - // older baseline still learns that the package is gone. + // older baseline still learns that the package is gone. The rowid is unknown + // because the package is no longer in the index; generation does not need it, + // as it resolves a removal against the baseline by identifier. Builder::StatementBuilder insertBuilder; insertBuilder.InsertInto(s_PUTT_Table_Name). Columns({ s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_IsRemoved }). @@ -155,6 +228,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 Utility::SHA256::HashBuffer manifestHash = Utility::SHA256::ComputeHash(compressedManifest); int64_t currentTime = Utility::GetCurrentUnixEpoch(); + // The rowid the package occupies is the identity that a delta is keyed on, so it is + // recorded alongside the data. It is resolved here rather than at removal time because + // the package is still in the index at this point. + SQLite::rowid_t packageRowId = 0; + + if (removals == RemovalBehavior::Record) + { + std::optional indexRowId = GetPackageRowIdInIndex(connection, packageIdentifier); + THROW_HR_IF(E_NOT_VALID_STATE, !indexRowId); + packageRowId = indexRowId.value(); + } + // First attempt to update the row and then insert it if no modification occurred. Builder::StatementBuilder updateBuilder; updateBuilder.Update(s_PUTT_Table_Name).Set(). @@ -170,14 +255,47 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 updateBuilder.Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); + if (removals == RemovalBehavior::Record) + { + // Match on the rowid as well, so that a re-add only revives the tombstone that + // refers to the rowid the package now occupies. A package removed and re-added + // elsewhere leaves its old tombstone intact, which is what tells a delta to + // suppress the baseline row it still holds. Restricting by rowid also prevents + // this update from overwriting a different package's tombstone that happens to + // name the same rowid. + updateBuilder.And(s_PUTT_PackageRowId).Equals(packageRowId); + } + updateBuilder.Execute(connection); if (connection.GetChanges() == 0) { Builder::StatementBuilder insertBuilder; - insertBuilder.InsertInto(s_PUTT_Table_Name). - Columns({ s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash }). - Values(packageIdentifier, currentTime, compressedManifest, manifestHash); + insertBuilder.InsertInto(s_PUTT_Table_Name).BeginColumns(); + + insertBuilder.Column(s_PUTT_Package); + insertBuilder.Column(s_PUTT_WriteTime); + insertBuilder.Column(s_PUTT_Manifest); + insertBuilder.Column(s_PUTT_Hash); + + if (removals == RemovalBehavior::Record) + { + insertBuilder.Column(s_PUTT_PackageRowId); + } + + insertBuilder.EndColumns().BeginValues(); + + insertBuilder.Value(packageIdentifier); + insertBuilder.Value(currentTime); + insertBuilder.Value(compressedManifest); + insertBuilder.Value(manifestHash); + + if (removals == RemovalBehavior::Record) + { + insertBuilder.Value(packageRowId); + } + + insertBuilder.EndValues(); insertBuilder.Execute(connection); } @@ -223,6 +341,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Any package recorded as removed must no longer be in the internal index for (const std::string& packageIdentifier : GetRemovalsSince(connection, 0, removals)) { + // A package that was removed and later re-added keeps the tombstone for the rowid it + // vacated alongside a live row for the rowid it now occupies. Its presence in the + // index is therefore expected, and only the live row describes it. + if (HasLiveRow(connection, packageIdentifier)) + { + continue; + } + SearchRequest request; request.Inclusions.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, packageIdentifier); @@ -239,6 +365,30 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } + // Every live row must name the rowid that the package actually occupies, since that is the + // identity a delta is keyed on. A disagreement means either this table or the rowid + // pinning performed during packaging has drifted. + if (removals == RemovalBehavior::Record) + { + for (const PackageData& packageData : GetUpdatesSince(connection, 0, removals)) + { + std::optional indexRowId = GetPackageRowIdInIndex(connection, packageData.PackageIdentifier); + + if (!indexRowId || indexRowId.value() != packageData.PackageRowId) + { + if (!log) + { + return false; + } + + result = false; + AICLI_LOG(Repo, Info, << " [INVALID] value [" << s_PUTT_PackageRowId << "] in table [" << s_PUTT_Table_Name << + "] at row [" << packageData.RowID << "]; the package [" << packageData.PackageIdentifier << "] records rowid [" << + packageData.PackageRowId << "] but occupies [" << (indexRowId ? std::to_string(indexRowId.value()) : "none") << "]"); + } + } + } + // Ensure that all packages in the internal index are present in the update table Builder::StatementBuilder builder; builder.Select(Builder::RowCount).From(s_PUTT_Table_Name).Where(s_PUTT_Package).Like(Builder::Unbound).Escape(EscapeCharForLike); @@ -278,11 +428,22 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 std::vector PackageUpdateTrackingTable::GetUpdatesSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals) { + bool recordingRemovals = (removals == RemovalBehavior::Record); + Builder::StatementBuilder builder; - builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash }). - From(s_PUTT_Table_Name).Where(s_PUTT_WriteTime).IsGreaterThanOrEqualTo(updateBaseTime); - if (removals == RemovalBehavior::Record) + if (recordingRemovals) + { + builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash, s_PUTT_PackageRowId }); + } + else + { + builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash }); + } + + builder.From(s_PUTT_Table_Name).Where(s_PUTT_WriteTime).IsGreaterThanOrEqualTo(updateBaseTime); + + if (recordingRemovals) { // Removals are reported separately, so that this remains the set of packages that // have data to write out, exactly as it is when removals delete their row. @@ -302,6 +463,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 item.Manifest = select.GetColumn(3); item.Hash = select.GetColumn(4); + if (recordingRemovals) + { + item.PackageRowId = select.GetColumn(5); + } + result.emplace_back(std::move(item)); } @@ -325,19 +491,37 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 Statement select = builder.Prepare(connection); + // A package that was removed, re-added at a different rowid, and removed again leaves a + // tombstone for each rowid it vacated. They all name the same package, and a consumer + // resolves that name against the baseline once, so report it once. Folding the case keeps + // this consistent with the ICU LIKE that decides package identity everywhere else. + std::set seen; + while (select.Step()) { - result.emplace_back(select.GetColumn(0)); + std::string packageIdentifier = select.GetColumn(0); + + if (seen.insert(Utility::FoldCase(static_cast(packageIdentifier))).second) + { + result.emplace_back(std::move(packageIdentifier)); + } } return result; } - SQLite::blob_t PackageUpdateTrackingTable::GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier) + SQLite::blob_t PackageUpdateTrackingTable::GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier, RemovalBehavior removals) { Builder::StatementBuilder builder; builder.Select(s_PUTT_Hash).From(s_PUTT_Table_Name).Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); + if (removals == RemovalBehavior::Record) + { + // A tombstone has its hash cleared, so restrict to the live row or this would be + // liable to return a null hash for a package that was removed and re-added. + builder.And(s_PUTT_IsRemoved).Equals(0); + } + Statement select = builder.Prepare(connection); THROW_HR_IF(E_NOT_SET, !select.Step()); @@ -345,17 +529,58 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return select.GetColumn(0); } - void PackageUpdateTrackingTable::AddIsRemovedColumn(SQLite::Connection& connection) + void PackageUpdateTrackingTable::AddRemovalTrackingColumns(SQLite::Connection& connection) { // The table is created on demand, so an index that has never had a manifest written - // to it will not have one yet. It will be created with the column when it is needed. + // to it will not have one yet. It will be created with the columns when it is needed. if (!Exists(connection)) { return; } - Builder::StatementBuilder builder; - builder.AlterTable(s_PUTT_Table_Name).Add(Builder::ColumnBuilder(s_PUTT_IsRemoved, Builder::Type::Int64).NotNull().Default(0)); - builder.Execute(connection); + Builder::StatementBuilder isRemovedBuilder; + isRemovedBuilder.AlterTable(s_PUTT_Table_Name).Add(Builder::ColumnBuilder(s_PUTT_IsRemoved, Builder::Type::Int64).NotNull().Default(0)); + isRemovedBuilder.Execute(connection); + + Builder::StatementBuilder packageRowIdBuilder; + packageRowIdBuilder.AlterTable(s_PUTT_Table_Name).Add(Builder::ColumnBuilder(s_PUTT_PackageRowId, Builder::Type::Int64).NotNull().Default(0)); + packageRowIdBuilder.Execute(connection); + + // Backfill the rowid for the rows already present. Every one of them is live: 2.0 deletes + // the row when a package is removed, so a table being migrated has no tombstones and every + // package it names is still in the index. + Builder::StatementBuilder selectBuilder; + selectBuilder.Select({ RowIDName, s_PUTT_Package }).From(s_PUTT_Table_Name); + + std::vector> rows; + + { + Statement select = selectBuilder.Prepare(connection); + + while (select.Step()) + { + rows.emplace_back(select.GetColumn(0), select.GetColumn(1)); + } + } + + Builder::StatementBuilder updateBuilder; + updateBuilder.Update(s_PUTT_Table_Name).Set(). + Column(s_PUTT_PackageRowId).Equals(Builder::Unbound). + Where(RowIDName).Equals(Builder::Unbound); + + Statement update = updateBuilder.Prepare(connection); + + for (const auto& row : rows) + { + std::optional packageRowId = GetPackageRowIdInIndex(connection, row.second); + THROW_HR_IF(E_NOT_VALID_STATE, !packageRowId); + + update.Reset(); + update.Bind(1, packageRowId.value()); + update.Bind(2, row.first); + update.Execute(); + } + + CreateLiveRowIndex(connection); } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h index d72b6eac54..a5f9edaba3 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h @@ -28,6 +28,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Creates the table. static void Create(SQLite::Connection& connection, RemovalBehavior removals); + // Creates the unique index that allows at most one live row per package rowid. + static void CreateLiveRowIndex(SQLite::Connection& connection); + // Creates the table if it does not exist. static void EnsureExists(SQLite::Connection& connection, RemovalBehavior removals); @@ -52,6 +55,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 int64_t WriteTime = 0; SQLite::blob_t Manifest; SQLite::blob_t Hash; + // The rowid the package occupies in the index, or 0 when it is not known. + // Only recorded when removals are being recorded; see the column comment. + SQLite::rowid_t PackageRowId = 0; }; // Gets the data on updates that have been written since the given base time. @@ -60,13 +66,15 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Gets the identifiers of the packages removed since the given base time. // Only meaningful when removals are being recorded; always empty otherwise. + // A package that was removed more than once contributes a single entry. static std::vector GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); // Gets the data hash for the given package identifier. - static SQLite::blob_t GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier); + static SQLite::blob_t GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier, RemovalBehavior removals); - // Adds the is_removed column to an existing table that does not have it. + // Adds the columns needed to record removals to an existing table that does not have them, + // and backfills the package rowid for the rows already present. // Used when migrating from schema 2.0 to 2.1; does nothing if the table does not exist. - static void AddIsRemovedColumn(SQLite::Connection& connection); + static void AddRemovalTrackingColumns(SQLite::Connection& connection); }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp index 788d737480..262d84aaad 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta @@ -327,6 +328,29 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta nextValueRowIds[table.TableName] = GetMaximumRowId(baselineConnection, table.TableName); } + // Resolve the changed packages first so that the removals can tell whether the rowid they + // are about to vacate has already been taken by one of them. + // + // The rowid comes from the source rather than the baseline so that packages new to this + // delta are covered by the same lookup; rowid stability is what makes the two agree. + struct ChangedPackage + { + const V2_0::PackageUpdateTrackingTable::PackageData* Data; + SQLite::rowid_t RowId; + }; + + std::vector changed; + std::set claimedRowIds; + + for (const auto& package : changedPackages) + { + std::optional packageRowId = SelectPackageRowId(sourceConnection, package.PackageIdentifier); + THROW_HR_IF(E_NOT_VALID_STATE, !packageRowId); + + changed.emplace_back(ChangedPackage{ &package, packageRowId.value() }); + claimedRowIds.insert(packageRowId.value()); + } + for (const std::string& packageIdentifier : removedPackages) { std::optional packageRowId = SelectPackageRowId(baselineConnection, packageIdentifier); @@ -339,30 +363,37 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta continue; } + if (claimedRowIds.count(packageRowId.value())) + { + // Another package has taken the rowid this one vacated. Recording the removal is + // both impossible, since the rowid is the primary key of the delta's package table, + // and unnecessary: the row written for the new occupant already suppresses the + // baseline row, and the association differences are computed against the baseline + // at that same rowid, so the old package's data is displaced entirely. + AICLI_LOG(Repo, Verbose, << "Delta: [" << packageIdentifier << "] was removed but its rowid " << + packageRowId.value() << " is now held by a changed package"); + continue; + } + AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << packageIdentifier << "] (rowid " << packageRowId.value() << ")"); WriteRemovedPackage(deltaConnection, packageRowId.value(), packageIdentifier); } - for (const auto& package : changedPackages) + for (const auto& package : changed) { - // The rowid comes from the source rather than the baseline so that packages new to this - // delta are covered by the same lookup; rowid stability is what makes the two agree. - std::optional packageRowId = SelectPackageRowId(sourceConnection, package.PackageIdentifier); - THROW_HR_IF(E_NOT_VALID_STATE, !packageRowId); - - AICLI_LOG(Repo, Verbose, << "Delta: recording change to [" << package.PackageIdentifier << "] (rowid " << packageRowId.value() << ")"); + AICLI_LOG(Repo, Verbose, << "Delta: recording change to [" << package.Data->PackageIdentifier << "] (rowid " << package.RowId << ")"); - WriteChangedPackage(deltaConnection, sourceConnection, packageRowId.value()); + WriteChangedPackage(deltaConnection, sourceConnection, package.RowId); for (const auto& table : SystemReferenceTables()) { - WriteSystemReferenceDifference(deltaConnection, sourceConnection, baselineConnection, table, packageRowId.value()); + WriteSystemReferenceDifference(deltaConnection, sourceConnection, baselineConnection, table, package.RowId); } for (const auto& table : OneToManyTables()) { - WriteOneToManyDifference(deltaConnection, sourceConnection, baselineConnection, table, packageRowId.value(), nextValueRowIds[table.TableName]); + WriteOneToManyDifference(deltaConnection, sourceConnection, baselineConnection, table, package.RowId, nextValueRowIds[table.TableName]); } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp index 54a014d6ba..1b1e837d7d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -39,7 +39,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 // Migration from 2.0 → 2.1: add the is_removed column to update_tracking. if (v2result || (currentVersion.MajorVersion == 2 && currentVersion.MinorVersion == 0)) { - V2_0::PackageUpdateTrackingTable::AddIsRemovedColumn(connection); + V2_0::PackageUpdateTrackingTable::AddRemovalTrackingColumns(connection); savepoint.Commit(); return true; } From 49ddd20de65638c4f25b72ce7b921531bdbf625a Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 4 Sep 2026 10:54:11 -0700 Subject: [PATCH 26/36] Reorder for efficiency --- .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp index 262d84aaad..640c53f68c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -328,27 +328,32 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta nextValueRowIds[table.TableName] = GetMaximumRowId(baselineConnection, table.TableName); } - // Resolve the changed packages first so that the removals can tell whether the rowid they - // are about to vacate has already been taken by one of them. - // - // The rowid comes from the source rather than the baseline so that packages new to this - // delta are covered by the same lookup; rowid stability is what makes the two agree. - struct ChangedPackage - { - const V2_0::PackageUpdateTrackingTable::PackageData* Data; - SQLite::rowid_t RowId; - }; - - std::vector changed; + // The changed packages are written first so that the removals can tell whether the rowid + // they are about to vacate has already been taken by one of them. std::set claimedRowIds; for (const auto& package : changedPackages) { + // The rowid comes from the source rather than the baseline so that packages new to this + // delta are covered by the same lookup; rowid stability is what makes the two agree. std::optional packageRowId = SelectPackageRowId(sourceConnection, package.PackageIdentifier); THROW_HR_IF(E_NOT_VALID_STATE, !packageRowId); - changed.emplace_back(ChangedPackage{ &package, packageRowId.value() }); + AICLI_LOG(Repo, Verbose, << "Delta: recording change to [" << package.PackageIdentifier << "] (rowid " << packageRowId.value() << ")"); + claimedRowIds.insert(packageRowId.value()); + + WriteChangedPackage(deltaConnection, sourceConnection, packageRowId.value()); + + for (const auto& table : SystemReferenceTables()) + { + WriteSystemReferenceDifference(deltaConnection, sourceConnection, baselineConnection, table, packageRowId.value()); + } + + for (const auto& table : OneToManyTables()) + { + WriteOneToManyDifference(deltaConnection, sourceConnection, baselineConnection, table, packageRowId.value(), nextValueRowIds[table.TableName]); + } } for (const std::string& packageIdentifier : removedPackages) @@ -380,23 +385,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta WriteRemovedPackage(deltaConnection, packageRowId.value(), packageIdentifier); } - for (const auto& package : changed) - { - AICLI_LOG(Repo, Verbose, << "Delta: recording change to [" << package.Data->PackageIdentifier << "] (rowid " << package.RowId << ")"); - - WriteChangedPackage(deltaConnection, sourceConnection, package.RowId); - - for (const auto& table : SystemReferenceTables()) - { - WriteSystemReferenceDifference(deltaConnection, sourceConnection, baselineConnection, table, package.RowId); - } - - for (const auto& table : OneToManyTables()) - { - WriteOneToManyDifference(deltaConnection, sourceConnection, baselineConnection, table, package.RowId, nextValueRowIds[table.TableName]); - } - } - savepoint.Commit(); // Outside the savepoint, since this vacuums. From 56fc7bff1f86f0b2f4c6339bfe90d2e278bdf54e Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 4 Sep 2026 11:08:43 -0700 Subject: [PATCH 27/36] build fixes --- .../Microsoft/Schema/2_1/DeltaTables.cpp | 6 ++++-- .../Microsoft/Schema/2_1/DeltaTables.h | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp index 510d46698b..b6cacc3c19 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp @@ -176,13 +176,15 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "delta_preparetables_v2_1"); StatementBuilder packagesBuilder; - packagesBuilder.DropIndex({ GetTableName(V2_0::PackagesTable::TableName()), s_Delta_ValueIndexSuffix }); + auto packagesTableName = GetTableName(V2_0::PackagesTable::TableName()); + packagesBuilder.DropIndex({ packagesTableName, s_Delta_ValueIndexSuffix }); packagesBuilder.Execute(connection); for (const auto& table : OneToManyTables()) { StatementBuilder builder; - builder.DropIndex({ GetTableName(table.TableName), s_Delta_ValueIndexSuffix }); + auto tableName = GetTableName(table.TableName); + builder.DropIndex({ tableName, s_Delta_ValueIndexSuffix }); builder.Execute(connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h index 6376652d83..5d2fa0179e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.h @@ -17,10 +17,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta }; // The tables that store a value directly alongside the package that it refers to. - const std::vector& SystemReferenceTables(); + std::vector SystemReferenceTables(); // The tables that store values in a data table, associated with packages through a map table. - const std::vector& OneToManyTables(); + std::vector OneToManyTables(); // Gets the name of the delta table that mirrors the given 2.0 table. // The delta tables are named distinctly so that a delta database can be attached alongside @@ -35,4 +35,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta // Creates the full set of delta tables in the given database. void CreateTables(SQLite::Connection& connection); + + // Performs the necessary packaging steps for the delta tables. + void PrepareTablesForPackaging(SQLite::Connection& connection); } From f1bf5eb20003ac702eff9f7a0969e285e382a97e Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 4 Sep 2026 13:44:01 -0700 Subject: [PATCH 28/36] Tests --- .../AppInstallerCLITests.vcxproj | 3 + src/AppInstallerCLITests/SQLiteIndex.cpp | 880 +-------- src/AppInstallerCLITests/SQLiteIndexDelta.cpp | 1572 +++++++++++++++++ .../SQLiteIndexTestCommon.cpp | 140 ++ .../SQLiteIndexTestCommon.h | 182 ++ 5 files changed, 1900 insertions(+), 877 deletions(-) create mode 100644 src/AppInstallerCLITests/SQLiteIndexDelta.cpp create mode 100644 src/AppInstallerCLITests/SQLiteIndexTestCommon.cpp create mode 100644 src/AppInstallerCLITests/SQLiteIndexTestCommon.h diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 8f624eb24d..7680564e42 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -212,6 +212,7 @@ + @@ -329,6 +330,8 @@ + + diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index dd9f0b0b22..02edf2e04b 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "SQLiteIndexTestCommon.h" #include #include #include @@ -9,7 +10,6 @@ #include #include #include -#include #include #include @@ -25,7 +25,6 @@ #include #include #include -#include #include #include @@ -40,22 +39,6 @@ using namespace AppInstaller::SQLite; using namespace AppInstaller::Utility; using UtilityVersion = AppInstaller::Utility::Version; -using SQLiteVersion = AppInstaller::SQLite::Version; - -SQLiteIndex CreateTestIndex(const std::string& filePath, std::optional version = {}) -{ - // If no specific version requested, then use generator to run against the last 3 versions. - if (!version) - { - SQLiteVersion latestVersion{ 2, 0 }; - SQLiteVersion versionMinus1 = SQLiteVersion{ 1, 7 }; - SQLiteVersion versionMinus2 = SQLiteVersion{ 1, 6 }; - - version = GENERATE_COPY(SQLiteVersion{ versionMinus2 }, SQLiteVersion{ versionMinus1 }, SQLiteVersion{ latestVersion }); - } - - return SQLiteIndex::CreateNew(filePath, version.value()); -} SQLiteVersion TestPrepareForRead(SQLiteIndex& index) { @@ -87,25 +70,9 @@ SQLiteVersion TestPrepareForRead(SQLiteIndex& index) return index.GetVersion(); } -std::string GetPathFromManifest(Manifest& manifest) -{ - auto publisher = manifest.Id; - AppInstaller::Utility::FindAndReplace(publisher, ".", "/"); - - return AppInstaller::Utility::ToLower(publisher).append("/").append(manifest.Version); -} -void CreateFakeManifest(Manifest& manifest, string_t publisher, string_t version = "1.0.0") -{ - manifest.Installers.push_back({}); - manifest.Id = publisher.append(".").append("Id"); - manifest.DefaultLocalization.Add(publisher.append(" Name")); - manifest.Moniker = "testmoniker"; - manifest.Version = version; - manifest.Channel = "test"; - manifest.DefaultLocalization.Add({ "t1", "t2" }); - manifest.Installers[0].Commands = { "test1", "test2" }; -} + + SQLiteIndex SimpleTestSetup(const std::string& filePath, Manifest& manifest, std::optional version = {}) { @@ -121,175 +88,6 @@ SQLiteIndex SimpleTestSetup(const std::string& filePath, Manifest& manifest, std return index; } -struct IndexFields -{ - IndexFields( - std::string id, - std::string name, - std::string moniker, - std::string version, - std::string channel, - std::vector tags, - std::vector commands, - std::string path - ) : - Id(std::move(id)), - Name(std::move(name)), - Moniker(std::move(moniker)), - Version(std::move(version)), - Channel(std::move(channel)), - Tags(std::move(tags)), - Commands(std::move(commands)), - Path(std::move(path)) - {} - - IndexFields( - std::string id, - std::string name, - std::string moniker, - std::string version, - std::string channel, - std::vector tags, - std::vector commands, - std::string path, - std::vector packageFamilyNames, - std::vector productCodes - ) : - Id(std::move(id)), - Name(std::move(name)), - Moniker(std::move(moniker)), - Version(std::move(version)), - Channel(std::move(channel)), - Tags(std::move(tags)), - Commands(std::move(commands)), - Path(std::move(path)), - PackageFamilyNames(std::move(packageFamilyNames)), - ProductCodes(std::move(productCodes)) - {} - - IndexFields( - std::string id, - std::string name, - std::string publisher, - std::string moniker, - std::string version, - std::string channel, - std::vector tags, - std::vector commands, - std::string path, - std::vector packageFamilyNames, - std::vector productCodes - ) : - Id(std::move(id)), - Name(std::move(name)), - Publisher(std::move(publisher)), - Moniker(std::move(moniker)), - Version(std::move(version)), - Channel(std::move(channel)), - Tags(std::move(tags)), - Commands(std::move(commands)), - Path(std::move(path)), - PackageFamilyNames(std::move(packageFamilyNames)), - ProductCodes(std::move(productCodes)) - {} - - IndexFields( - std::string id, - std::string name, - std::string publisher, - std::string moniker, - std::string version, - std::string channel, - std::vector tags, - std::vector commands, - std::string path, - std::vector packageFamilyNames, - std::vector productCodes, - std::string arpName, - std::string arpPublisher - ) : - Id(std::move(id)), - Name(std::move(name)), - Publisher(std::move(publisher)), - Moniker(std::move(moniker)), - Version(std::move(version)), - Channel(std::move(channel)), - Tags(std::move(tags)), - Commands(std::move(commands)), - Path(std::move(path)), - PackageFamilyNames(std::move(packageFamilyNames)), - ProductCodes(std::move(productCodes)), - ArpName(std::move(arpName)), - ArpPublisher(std::move(arpPublisher)) - {} - - std::string Id; - std::string Name; - std::string Publisher; - std::string Moniker; - std::string Version; - std::string Channel; - std::vector Tags; - std::vector Commands; - std::string Path; - std::vector PackageFamilyNames; - std::vector ProductCodes; - std::string ArpName; - std::string ArpPublisher; -}; - -SQLiteIndex SearchTestSetup(const std::string& filePath, std::initializer_list data = {}, std::optional version = {}) -{ - SQLiteIndex index = CreateTestIndex(filePath, version); - - Manifest manifest; - - auto addFunc = [&](const IndexFields& d) - { - manifest.Id = d.Id; - manifest.DefaultLocalization.Add(d.Name); - manifest.DefaultLocalization.Add(d.Publisher); - manifest.Moniker = d.Moniker; - manifest.Version = d.Version; - manifest.DefaultLocalization.Add(d.Tags); - - manifest.Installers.resize(std::max(d.PackageFamilyNames.size(), d.ProductCodes.size())); - - if (manifest.Installers.size() == 0) - { - manifest.Installers.push_back({}); - } - - manifest.Channel = d.Channel; - manifest.Installers[0].Commands = d.Commands; - - for (size_t i = 0; i < d.PackageFamilyNames.size(); ++i) - { - manifest.Installers[i].PackageFamilyName = d.PackageFamilyNames[i]; - } - - for (size_t i = 0; i < d.ProductCodes.size(); ++i) - { - manifest.Installers[i].ProductCode = d.ProductCodes[i]; - } - - if (!d.ArpName.empty() || !d.ArpPublisher.empty()) - { - manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); - manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayName = d.ArpName; - manifest.Installers[0].AppsAndFeaturesEntries[0].Publisher = d.ArpPublisher; - } - - index.AddManifest(manifest, d.Path); - }; - - for (const auto& d : data) - { - addFunc(d); - } - - return index; -} bool ArePackageFamilyNameAndProductCodeSupported(const SQLiteIndex& index, const SQLiteVersion& testVersion) { @@ -3612,38 +3410,6 @@ TEST_CASE("SQLiteIndex_Property_IntermediateFilePath", "[sqliteindex]") REQUIRE(contextData.Get() == intermediateFilePath); } -struct ManifestAndPath -{ - Manifest Manifest; - std::string Path; -}; - -void CreateFakeManifestAndPath( - ManifestAndPath& manifestAndPath, - const string_t& publisher, - std::string_view version = "1.0.0", - std::optional arpMinVersion = {}, - std::optional arpMaxVersion = {}) -{ - CreateFakeManifest(manifestAndPath.Manifest, publisher, version); - manifestAndPath.Path = ConvertToUTF8(CreateNewGuidNameWString()); - manifestAndPath.Manifest.StreamSha256 = SHA256::ComputeHash(manifestAndPath.Path); - - if (arpMinVersion) - { - manifestAndPath.Manifest.Installers[0].BaseInstallerType = InstallerTypeEnum::Exe; - manifestAndPath.Manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); - manifestAndPath.Manifest.Installers[0].AppsAndFeaturesEntries.back().DisplayVersion = arpMinVersion.value(); - } - - if (arpMaxVersion) - { - manifestAndPath.Manifest.Installers[0].BaseInstallerType = InstallerTypeEnum::Exe; - manifestAndPath.Manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); - manifestAndPath.Manifest.Installers[0].AppsAndFeaturesEntries.back().DisplayVersion = arpMaxVersion.value(); - } -} - std::filesystem::path GetOnlyChild(const std::filesystem::path& parent) { auto parentDirectoryIterator = std::filesystem::directory_iterator{ parent }; @@ -3969,643 +3735,3 @@ TEST_CASE("SQLiteIndex_VersionStringPreserved", "[sqliteindex]") REQUIRE(extractedVersion == version); } -namespace -{ - // Reads the rowid assigned to a package identifier in a prepared 2.0 index. - int64_t GetPreparedPackageRowId(const std::filesystem::path& indexPath, std::string_view packageIdentifier) - { - Connection connection = Connection::Create(indexPath.u8string(), Connection::OpenDisposition::ReadOnly); - Statement statement = Statement::Create(connection, "SELECT rowid FROM packages WHERE id = ?"); - statement.Bind(1, std::string{ packageIdentifier }); - REQUIRE(statement.Step()); - return statement.GetColumn(0); - } -} - -// The delta index relies on a package receiving the same packages rowid every time the same -// working index is prepared, even as other packages are added and removed around it. -TEST_CASE("SQLiteIndex_PrepareForPackaging_RowIdsAreStable", "[sqliteindex][V2_0]") -{ - TempFile workingFile{ "rowid_working"s, ".db"s }; - TempFile firstFile{ "rowid_first"s, ".db"s }; - TempFile secondFile{ "rowid_second"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - ManifestAndPath m3; - CreateFakeManifestAndPath(m3, "Publisher3", "1.0"); - ManifestAndPath m4; - CreateFakeManifestAndPath(m4, "Publisher4", "1.0"); - - { - SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 0 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - index.AddManifest(m2.Manifest, m2.Path); - index.AddManifest(m3.Manifest, m3.Path); - } - - std::filesystem::copy_file(workingFile.GetPath(), firstFile.GetPath(), std::filesystem::copy_options::overwrite_existing); - - { - SQLiteIndex prepared = SQLiteIndex::Open(firstFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - prepared.PrepareForPackaging(); - } - - // Remove the first package and add a new one, which would renumber the survivors without pinning. - { - SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - index.RemoveManifest(m1.Manifest, m1.Path); - index.AddManifest(m4.Manifest, m4.Path); - } - - std::filesystem::copy_file(workingFile.GetPath(), secondFile.GetPath(), std::filesystem::copy_options::overwrite_existing); - - { - SQLiteIndex prepared = SQLiteIndex::Open(secondFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - prepared.PrepareForPackaging(); - } - - REQUIRE(GetPreparedPackageRowId(firstFile.GetPath(), "Publisher2.Id") == GetPreparedPackageRowId(secondFile.GetPath(), "Publisher2.Id")); - REQUIRE(GetPreparedPackageRowId(firstFile.GetPath(), "Publisher3.Id") == GetPreparedPackageRowId(secondFile.GetPath(), "Publisher3.Id")); - - // The added package must not collide with any existing rowid. - REQUIRE(GetPreparedPackageRowId(secondFile.GetPath(), "Publisher4.Id") > GetPreparedPackageRowId(secondFile.GetPath(), "Publisher3.Id")); -} - -TEST_CASE("SQLiteIndex_UpdateTracking_V2_0_RemovalDeletesRow", "[sqliteindex][V2_0][updatetracking]") -{ - TempFile indexFile{ "updatetracking"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - - { - SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 0 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - index.AddManifest(m2.Manifest, m2.Path); - index.RemoveManifest(m2.Manifest, m2.Path); - REQUIRE(index.CheckConsistency(true)); - } - - Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadWrite); - using Tracking = Schema::V2_0::PackageUpdateTrackingTable; - - // 2.0 deletes the row, so the removed package leaves no trace at all. - auto updates = Tracking::GetUpdatesSince(connection, 0, Tracking::RemovalBehavior::Delete); - REQUIRE(updates.size() == 1); - REQUIRE(updates[0].PackageIdentifier == "Publisher1.Id"); - - // Read with Record to check on the Delete - REQUIRE(Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Delete).empty()); -} - -TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_RemovalIsRecorded", "[sqliteindex][V2_1][updatetracking]") -{ - TempFile indexFile{ "updatetracking"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - - { - SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 1 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - index.AddManifest(m2.Manifest, m2.Path); - index.RemoveManifest(m2.Manifest, m2.Path); - REQUIRE(index.CheckConsistency(true)); - } - - Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadWrite); - using Tracking = Schema::V2_0::PackageUpdateTrackingTable; - - // The removal must not appear as an update; that would change what the version data - // manifest export writes out, which is 2.0 behavior that 2.1 preserves exactly. - auto updates = Tracking::GetUpdatesSince(connection, 0, Tracking::RemovalBehavior::Record); - REQUIRE(updates.size() == 1); - REQUIRE(updates[0].PackageIdentifier == "Publisher1.Id"); - - auto removals = Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record); - REQUIRE(removals.size() == 1); - REQUIRE(removals[0] == "Publisher2.Id"); -} - -TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_ReAddClearsRemoval", "[sqliteindex][V2_1][updatetracking]") -{ - TempFile indexFile{ "updatetracking"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - - { - SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 1 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - index.AddManifest(m2.Manifest, m2.Path); - index.RemoveManifest(m2.Manifest, m2.Path); - index.AddManifest(m2.Manifest, m2.Path); - REQUIRE(index.CheckConsistency(true)); - } - - Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadWrite); - using Tracking = Schema::V2_0::PackageUpdateTrackingTable; - - auto updates = Tracking::GetUpdatesSince(connection, 0, Tracking::RemovalBehavior::Record); - REQUIRE(updates.size() == 2); - - REQUIRE(Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record).empty()); -} - -TEST_CASE("SQLiteIndex_UpdateTracking_V2_1_MigrateFrom_2_0", "[sqliteindex][V2_1][updatetracking]") -{ - TempFile indexFile{ "updatetracking"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - - { - SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 0 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - index.AddManifest(m2.Manifest, m2.Path); - } - - { - SQLiteIndex index = SQLiteIndex::Open(indexFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - REQUIRE(index.MigrateTo(SQLiteVersion{ 2, 1 })); - REQUIRE(index.GetVersion() == SQLiteVersion{ 2, 1 }); - - // Removals recorded after migration require the column added by the migration. - index.RemoveManifest(m2.Manifest, m2.Path); - REQUIRE(index.CheckConsistency(true)); - } - - Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadWrite); - using Tracking = Schema::V2_0::PackageUpdateTrackingTable; - - auto removals = Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record); - REQUIRE(removals.size() == 1); - REQUIRE(removals[0] == "Publisher2.Id"); -} - -TEST_CASE("SQLiteIndex_Delta_AddedPackage", "[sqliteindex][V2_1][delta]") -{ - TempFile workingFile{ "delta_working"s, ".db"s }; - TempFile baselineFile{ "delta_baseline"s, ".db"s }; - TempFile deltaFile{ "delta_output"s, ".db"s }; - - // Build baseline: one package "Publisher1" - { - SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - index.AddManifest(m1.Manifest, m1.Path); - - std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); - SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - prepared.PrepareForPackaging(); - prepared.MarkAsBaseline(); - } - - // Add a new package to the working index and generate a delta - { - SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); - - // Sleep 1s to ensure Publisher2's write time is after the new base time - std::this_thread::sleep_for(std::chrono::seconds(1)); - - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - index.AddManifest(m2.Manifest, m2.Path); - - index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); - index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); - index.PrepareForPackaging(); - } - - // The delta file should exist and contain the new package - REQUIRE(std::filesystem::exists(deltaFile.GetPath())); - - Connection deltaConn = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); - - Statement countStmt = Statement::Create(deltaConn, "SELECT COUNT(*) FROM delta_packages WHERE is_removed = 0"); - REQUIRE(countStmt.Step()); - REQUIRE(countStmt.GetColumn(0) == 1); - - Statement idStmt = Statement::Create(deltaConn, "SELECT id FROM delta_packages WHERE is_removed = 0"); - REQUIRE(idStmt.Step()); - REQUIRE(idStmt.GetColumn(0) == "Publisher2.Id"); -} - -TEST_CASE("SQLiteIndex_Delta_RemovedPackage", "[sqliteindex][V2_1][delta]") -{ - TempFile workingFile{ "delta_working"s, ".db"s }; - TempFile baselineFile{ "delta_baseline"s, ".db"s }; - TempFile deltaFile{ "delta_output"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - - // Build baseline: two packages - { - SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - index.AddManifest(m2.Manifest, m2.Path); - - std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); - SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - prepared.PrepareForPackaging(); - prepared.MarkAsBaseline(); - } - - // Remove Publisher2 from the working index and generate a delta - { - SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); - - // Sleep 1s to ensure Publisher2 removal write time is after the new base time - std::this_thread::sleep_for(std::chrono::seconds(1)); - - index.RemoveManifest(m2.Manifest, m2.Path); - - index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); - index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); - index.PrepareForPackaging(); - } - - REQUIRE(std::filesystem::exists(deltaFile.GetPath())); - - Connection deltaConn = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); - - Statement countStmt = Statement::Create(deltaConn, "SELECT COUNT(*) FROM delta_packages WHERE is_removed = 1"); - REQUIRE(countStmt.Step()); - REQUIRE(countStmt.GetColumn(0) == 1); - - Statement idStmt = Statement::Create(deltaConn, "SELECT id FROM delta_packages WHERE is_removed = 1"); - REQUIRE(idStmt.Step()); - REQUIRE(idStmt.GetColumn(0) == "Publisher2.Id"); -} - -TEST_CASE("SQLiteIndex_Delta_NoChanges_EmptyDelta", "[sqliteindex][V2_1][delta]") -{ - TempFile workingFile{ "delta_working"s, ".db"s }; - TempFile baselineFile{ "delta_baseline"s, ".db"s }; - TempFile deltaFile{ "delta_output"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - - // Build baseline - { - SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - - std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); - SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - prepared.PrepareForPackaging(); - prepared.MarkAsBaseline(); - } - - // Set tracking base to "now" so no packages appear changed - { - SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); // records current time - - index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); - index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); - index.PrepareForPackaging(); - } - - // A delta is produced even with nothing to report, so that a consumer never has to handle a - // missing delta as a special case. It is simply empty. - REQUIRE(std::filesystem::exists(deltaFile.GetPath())); - - Connection deltaConn = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); - - // The delta is a database in its own right, carrying the metadata that any index has. Without - // it the delta could not be opened, since opening reads the schema version to pick an interface. - REQUIRE(MetadataTable::GetNamedValue(deltaConn, AppInstaller::SQLite::s_MetadataValueName_MajorVersion) == 2); - REQUIRE(MetadataTable::GetNamedValue(deltaConn, AppInstaller::SQLite::s_MetadataValueName_MinorVersion) == 1); - REQUIRE(!MetadataTable::TryGetNamedValue(deltaConn, AppInstaller::SQLite::s_MetadataValueName_DatabaseIdentifier).value_or(std::string{}).empty()); - REQUIRE(MetadataTable::TryGetNamedValue(deltaConn, AppInstaller::SQLite::s_MetadataValueName_LastWriteTime).has_value()); - - for (std::string_view tableName : { - "delta_packages"sv, - "delta_pfns2"sv, "delta_productcodes2"sv, "delta_norm_names2"sv, "delta_norm_publishers2"sv, "delta_upgradecodes2"sv, - "delta_tags2"sv, "delta_tags2_map"sv, "delta_commands2"sv, "delta_commands2_map"sv }) - { - INFO(tableName); - - Statement countStmt = Statement::Create(deltaConn, "SELECT COUNT(*) FROM [" + std::string{ tableName } + "]"); - REQUIRE(countStmt.Step()); - REQUIRE(countStmt.GetColumn(0) == 0); - } - - // The delta ships as plain tables, just as a prepared 2.0 index does. Its indexes serve only - // generation, and the merged views probe nothing but primary keys. - Statement indexStmt = Statement::Create(deltaConn, "SELECT COUNT(*) FROM [sqlite_master] WHERE [type] = 'index'"); - REQUIRE(indexStmt.Step()); - REQUIRE(indexStmt.GetColumn(0) == 0); -} - -// Reads a package's tags through the merged delta views. This is the shape that the 2.0 search -// path reads in: the map table governs which values a package has, and the value table holds the -// strings themselves. -std::set GetTagsThroughDeltaViews(Connection& connection, std::string_view packageId) -{ - std::set result; - - Statement statement = Statement::Create(connection, - "SELECT [t].[tag] FROM [tags2] AS [t] " - "JOIN [tags2_map] AS [m] ON [m].[tag] = [t].[rowid] " - "JOIN [packages] AS [p] ON [p].[rowid] = [m].[package] " - "WHERE [p].[id] = ?"); - statement.Bind(1, std::string{ packageId }); - - while (statement.Step()) - { - result.insert(statement.GetColumn(0)); - } - - return result; -} - -TEST_CASE("SQLiteIndex_Delta_MergedViews_AssociationsAreSuppressedPerRow", "[sqliteindex][V2_1][delta]") -{ - TempFile workingFile{ "delta_working"s, ".db"s }; - TempFile baselineFile{ "delta_baseline"s, ".db"s }; - TempFile deltaFile{ "delta_output"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - - // Both packages start out with the tags that a fake manifest carries: t1 and t2. - { - SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - index.AddManifest(m2.Manifest, m2.Path); - - std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); - SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - prepared.PrepareForPackaging(); - prepared.MarkAsBaseline(); - } - - // Publisher1 trades t2 for t3, keeping t1. Publisher2 goes away entirely. - { - SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); - - std::this_thread::sleep_for(std::chrono::seconds(1)); - - m1.Manifest.DefaultLocalization.Add({ "t1", "t3" }); - REQUIRE(index.UpdateManifest(m1.Manifest, m1.Path)); - - index.RemoveManifest(m2.Manifest, m2.Path); - - index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); - index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); - index.PrepareForPackaging(); - } - - REQUIRE(std::filesystem::exists(deltaFile.GetPath())); - - Connection deltaConnection = Connection::Create(deltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); - Schema::V2_1::Delta::SetupReadMode(deltaConnection, DatabaseSpecifier{ baselineFile.GetPath().u8string(), DatabaseDisposition::Read }); - - // The delta records only what changed about Publisher1, so it never mentions t1 at all. - // Suppressing the baseline at the level of the package would therefore lose it. - REQUIRE(GetTagsThroughDeltaViews(deltaConnection, "Publisher1.Id") == std::set{ "t1", "t3" }); - - // A removed package gets no per-association removal rows, so the only thing that can suppress - // its associations is the removal recorded against the package itself. - REQUIRE(GetTagsThroughDeltaViews(deltaConnection, "Publisher2.Id").empty()); -} - -TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_Search", "[sqliteindex][V2_1][delta]") -{ - // Immutable is the disposition the shipped path uses, and it is the one that reaches SQLite as - // a URI rather than a plain path, so both files have to be named that way for the attach to - // resolve at all. - auto disposition = GENERATE(SQLiteStorageBase::OpenDisposition::Read, SQLiteStorageBase::OpenDisposition::Immutable); - - TempFile workingFile{ "delta_working"s, ".db"s }; - TempFile baselineFile{ "delta_baseline"s, ".db"s }; - TempFile workingFile2{ "delta_working2"s, ".db"s }; - TempFile deltaFile{ "delta_output"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - - // Build baseline with Publisher1 - { - SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - - std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); - SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - prepared.PrepareForPackaging(); - prepared.MarkAsBaseline(); - } - - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - - // Add Publisher2 to working copy and generate delta - { - SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); - - std::this_thread::sleep_for(std::chrono::seconds(1)); - - index.AddManifest(m2.Manifest, m2.Path); - - index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); - index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); - index.PrepareForPackaging(); - } - - REQUIRE(std::filesystem::exists(deltaFile.GetPath())); - - // Open the delta combined with the baseline - SQLiteIndex combined = SQLiteIndex::OpenWithBaseline( - deltaFile.GetPath().u8string(), - baselineFile.GetPath().u8string(), - disposition); - - // Search should return both Publisher1 (from baseline) and Publisher2 (from delta) - auto results = combined.Search({}); - REQUIRE(results.Matches.size() == 2); - - std::set foundIds; - for (const auto& match : results.Matches) - { - auto id = combined.GetPropertyByPrimaryId(match.first, PackageVersionProperty::Id); - REQUIRE(id.has_value()); - foundIds.insert(id.value()); - } - - REQUIRE(foundIds.count("Publisher1.Id") == 1); - REQUIRE(foundIds.count("Publisher2.Id") == 1); -} - -TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_RemovedPackageExcluded", "[sqliteindex][V2_1][delta]") -{ - TempFile workingFile{ "delta_working"s, ".db"s }; - TempFile baselineFile{ "delta_baseline"s, ".db"s }; - TempFile deltaFile{ "delta_output"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - - // Build baseline with two packages - { - SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - index.AddManifest(m2.Manifest, m2.Path); - - std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); - SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - prepared.PrepareForPackaging(); - prepared.MarkAsBaseline(); - } - - // Remove Publisher2 and generate delta - { - SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); - - std::this_thread::sleep_for(std::chrono::seconds(1)); - - index.RemoveManifest(m2.Manifest, m2.Path); - - index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); - index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); - index.PrepareForPackaging(); - } - - REQUIRE(std::filesystem::exists(deltaFile.GetPath())); - - // Open combined: should show only Publisher1 (Publisher2 removed) - SQLiteIndex combined = SQLiteIndex::OpenWithBaseline( - deltaFile.GetPath().u8string(), - baselineFile.GetPath().u8string()); - - auto results = combined.Search({}); - REQUIRE(results.Matches.size() == 1); - - auto id = combined.GetPropertyByPrimaryId(results.Matches[0].first, PackageVersionProperty::Id); - REQUIRE(id.has_value()); - REQUIRE(id.value() == "Publisher1.Id"); -} - -TEST_CASE("SQLiteIndex_Delta_UnmarkedBaselineRejected", "[sqliteindex][V2_1][delta]") -{ - TempFile workingFile{ "delta_working"s, ".db"s }; - TempFile baselineFile{ "delta_baseline"s, ".db"s }; - TempFile deltaFile{ "delta_output"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - - // Prepare a baseline but never designate it as one, so it has no identity for a delta to name. - { - SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - - std::filesystem::copy_file(workingFile.GetPath(), baselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); - SQLiteIndex prepared = SQLiteIndex::Open(baselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - prepared.PrepareForPackaging(); - } - - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - - SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); - - std::this_thread::sleep_for(std::chrono::seconds(1)); - - index.AddManifest(m2.Manifest, m2.Path); - - index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); - index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); - - REQUIRE_THROWS_HR(index.PrepareForPackaging(), APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); -} - -TEST_CASE("SQLiteIndex_Delta_MismatchedBaselineRejected", "[sqliteindex][V2_1][delta]") -{ - TempFile workingFile{ "delta_working"s, ".db"s }; - TempFile baselineFile{ "delta_baseline"s, ".db"s }; - TempFile otherBaselineFile{ "delta_baseline_other"s, ".db"s }; - TempFile deltaFile{ "delta_output"s, ".db"s }; - - ManifestAndPath m1; - CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); - - // Two baselines with identical contents. Designating each gives it its own identity, which is - // the point: a delta is tied to the baseline it was computed from, not to data that looks like it. - { - SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 1 }); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); - index.AddManifest(m1.Manifest, m1.Path); - - for (TempFile* file : { &baselineFile, &otherBaselineFile }) - { - std::filesystem::copy_file(workingFile.GetPath(), file->GetPath(), std::filesystem::copy_options::overwrite_existing); - SQLiteIndex prepared = SQLiteIndex::Open(file->GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); - prepared.PrepareForPackaging(); - prepared.MarkAsBaseline(); - } - } - - ManifestAndPath m2; - CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); - - { - SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); - - std::this_thread::sleep_for(std::chrono::seconds(1)); - - index.AddManifest(m2.Manifest, m2.Path); - - index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, baselineFile.GetPath().u8string()); - index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, deltaFile.GetPath().u8string()); - index.PrepareForPackaging(); - } - - // The baseline it was generated against opens fine. - REQUIRE_NOTHROW(SQLiteIndex::OpenWithBaseline(deltaFile.GetPath().u8string(), baselineFile.GetPath().u8string())); - - REQUIRE_THROWS_HR( - SQLiteIndex::OpenWithBaseline(deltaFile.GetPath().u8string(), otherBaselineFile.GetPath().u8string()), - APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); -} \ No newline at end of file diff --git a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp new file mode 100644 index 0000000000..6b00b935f2 --- /dev/null +++ b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp @@ -0,0 +1,1572 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "SQLiteIndexTestCommon.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::string_literals; +using namespace std::string_view_literals; +using namespace TestCommon; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::SQLite; +using namespace AppInstaller::Utility; + +using Tracking = Schema::V2_0::PackageUpdateTrackingTable; +namespace Delta = AppInstaller::Repository::Microsoft::Schema::V2_1::Delta; + +namespace +{ + // The first version that can produce or consume a delta. + const SQLiteVersion s_DeltaVersion{ 2, 1 }; + + // Runs a query that produces a single integer. + int64_t GetScalar(const Connection& connection, const std::string& sql) + { + Statement statement = Statement::Create(connection, sql); + REQUIRE(statement.Step()); + return statement.GetColumn(0); + } + + // Counts the rows in a table. + int64_t GetRowCount(const Connection& connection, std::string_view tableName) + { + return GetScalar(connection, "SELECT COUNT(*) FROM [" + std::string{ tableName } + "]"); + } + + // Reads every value in a single column of a query as a set. + std::set GetStrings(const Connection& connection, const std::string& sql) + { + std::set result; + Statement statement = Statement::Create(connection, sql); + + while (statement.Step()) + { + result.insert(statement.GetColumn(0)); + } + + return result; + } + + // Reads the rowid that a prepared index gave a package identifier. + std::optional GetPreparedPackageRowId(const std::filesystem::path& indexPath, std::string_view packageIdentifier) + { + Connection connection = Connection::Create(indexPath.u8string(), Connection::OpenDisposition::ReadOnly); + Statement statement = Statement::Create(connection, "SELECT rowid FROM packages WHERE id = ?"); + statement.Bind(1, std::string{ packageIdentifier }); + + if (statement.Step()) + { + return statement.GetColumn(0); + } + + return {}; + } + + // Reads a package's associated values through whatever the given connection calls the tables. + // + // Against a combined connection these names resolve to the merged views, so this reads exactly + // what the 2.0 search path would: the map decides which values a package has, and the value + // table holds the strings. + std::set GetOneToManyValues( + const Connection& connection, + std::string_view tableName, + std::string_view valueName, + std::string_view packageId) + { + std::string sql = + "SELECT [v].[" + std::string{ valueName } + "] FROM [" + std::string{ tableName } + "] AS [v] " + "JOIN [" + std::string{ tableName } + "_map] AS [m] ON [m].[" + std::string{ valueName } + "] = [v].[rowid] " + "JOIN [packages] AS [p] ON [p].[rowid] = [m].[package] " + "WHERE [p].[id] = '" + std::string{ packageId } + "'"; + + return GetStrings(connection, sql); + } + + // Reads a package's system reference values, which are stored directly against the package. + std::set GetSystemReferenceValues( + const Connection& connection, + std::string_view tableName, + std::string_view valueName, + std::string_view packageId) + { + std::string sql = + "SELECT [s].[" + std::string{ valueName } + "] FROM [" + std::string{ tableName } + "] AS [s] " + "JOIN [packages] AS [p] ON [p].[rowid] = [s].[package] " + "WHERE [p].[id] = '" + std::string{ packageId } + "'"; + + return GetStrings(connection, sql); + } + + // Drives the delta workflow, which is otherwise 40 lines of identical ceremony per test. + // + // The shape is fixed by what generation needs: a working index that accumulates changes, a + // designated baseline copied out of it at a chosen point, and a delta produced by preparing the + // working index afterwards. Preparing the working index also leaves it as an ordinary full + // index, which is what the equivalence tests compare the combined form against. + struct DeltaTestContext + { + TempFile WorkingFile{ "delta_working"s, ".db"s }; + TempFile BaselineFile{ "delta_baseline"s, ".db"s }; + TempFile DeltaFile{ "delta_output"s, ".db"s }; + + DeltaTestContext() = default; + + // Creates the working index and fills it with the data the baseline will hold. + DeltaTestContext(std::initializer_list baselineData) + { + CreateWorking(baselineData); + CaptureBaseline(); + } + + void CreateWorking(std::initializer_list baselineData) + { + SQLiteIndex index = SQLiteIndex::CreateNew(WorkingFile, s_DeltaVersion); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + + for (const auto& fields : baselineData) + { + Manifest manifest = CreateManifest(fields); + index.AddManifest(manifest, fields.Path); + } + } + + // Copies the working index out, prepares it, and designates it as a baseline. + // Everything the working index does afterwards is what the delta will describe. + void CaptureBaseline(bool markAsBaseline = true) + { + std::filesystem::copy_file(WorkingFile.GetPath(), BaselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + + SQLiteIndex prepared = SQLiteIndex::Open(BaselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + + if (markAsBaseline) + { + prepared.MarkAsBaseline(); + } + + m_baselineCaptured = true; + } + + // Opens the working index for the changes that the delta will carry. + // + // The base time is reset to now so that only what follows is considered changed, and the + // sleep is what makes that boundary observable: the tracking table stores whole seconds. + SQLiteIndex OpenWorkingForChanges() + { + SQLiteIndex index = SQLiteIndex::Open(WorkingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + + if (!m_baseTimeReset) + { + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); + std::this_thread::sleep_for(std::chrono::seconds(1)); + m_baseTimeReset = true; + } + + return index; + } + + void Add(const IndexFields& fields) + { + SQLiteIndex index = OpenWorkingForChanges(); + Manifest manifest = CreateManifest(fields); + index.AddManifest(manifest, fields.Path); + } + + void Update(const IndexFields& fields) + { + SQLiteIndex index = OpenWorkingForChanges(); + Manifest manifest = CreateManifest(fields); + REQUIRE(index.UpdateManifest(manifest, fields.Path)); + } + + void Remove(const IndexFields& fields) + { + SQLiteIndex index = OpenWorkingForChanges(); + Manifest manifest = CreateManifest(fields); + index.RemoveManifest(manifest, fields.Path); + } + + // Prepares the working index, producing the delta as a side effect. + void GenerateDelta() + { + REQUIRE(m_baselineCaptured); + + SQLiteIndex index = SQLiteIndex::Open(WorkingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, BaselineFile.GetPath().u8string()); + index.SetProperty(SQLiteIndex::Property::DeltaOutputPath, DeltaFile.GetPath().u8string()); + index.PrepareForPackaging(); + + m_deltaGenerated = true; + } + + // Opens the delta on its own, to inspect what generation actually wrote. + Connection OpenDeltaConnection() + { + REQUIRE(std::filesystem::exists(DeltaFile.GetPath())); + return Connection::Create(DeltaFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + } + + // Opens the delta with its baseline attached and the merged views in place, without going + // through SQLiteIndex, so that a test can read the merged tables directly. + Connection OpenMergedConnection() + { + Connection connection = OpenDeltaConnection(); + Delta::SetupReadMode(connection, DatabaseSpecifier{ BaselineFile.GetPath().u8string(), DatabaseDisposition::Read }); + return connection; + } + + SQLiteIndex OpenCombined(SQLiteStorageBase::OpenDisposition disposition = SQLiteStorageBase::OpenDisposition::Read) + { + REQUIRE(m_deltaGenerated); + return SQLiteIndex::OpenWithBaseline(DeltaFile.GetPath().u8string(), BaselineFile.GetPath().u8string(), disposition); + } + + // The working index, once prepared, is an ordinary full index built from the same data that + // the delta describes. That makes it the reference for equivalence. + SQLiteIndex OpenFullIndex() + { + REQUIRE(m_deltaGenerated); + return SQLiteIndex::Open(WorkingFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + } + + std::string DeltaTable(std::string_view baseTableName) const + { + return Delta::GetTableName(baseTableName); + } + + std::string DeltaMapTable(std::string_view baseTableName) const + { + return Delta::GetMapTableName(baseTableName); + } + + private: + bool m_baselineCaptured = false; + bool m_baseTimeReset = false; + bool m_deltaGenerated = false; + }; + + // A package with everything the index can store, so that the system reference tables are + // actually populated. The default fake manifest sets none of them. + IndexFields MakePackage( + std::string id, + std::string name, + std::vector tags = { "t1", "t2" }, + std::vector commands = { "c1" }, + std::vector packageFamilyNames = {}, + std::vector productCodes = {}) + { + return IndexFields{ + id, + std::move(name), + "Publisher"s, + "moniker"s, + "1.0"s, + ""s, + std::move(tags), + std::move(commands), + id + "/1.0", + std::move(packageFamilyNames), + std::move(productCodes) }; + } + + // Collects the identifiers that a search returns. + std::set GetSearchedIds(const SQLiteIndex& index, const SearchRequest& request = {}) + { + std::set result; + + for (const auto& match : index.Search(request).Matches) + { + auto id = index.GetPropertyByPrimaryId(match.first, PackageVersionProperty::Id); + REQUIRE(id.has_value()); + result.insert(id.value()); + } + + return result; + } +} + +// --------------------------------------------------------------------------------------------- +// Group B - package rowid identity +// +// The merged packages view suppresses a baseline row when the delta names the same rowid, and +// every association refers to a package by that rowid. These cases cover the ways that identity +// can be broken. +// --------------------------------------------------------------------------------------------- + +// B1. A package removed and re-added lands on a new rowid, so the delta has to both introduce it +// at the new rowid and vacate the old one. Recording only the add leaves the baseline row in +// place, and the package appears twice. +TEST_CASE("SQLiteIndex_Delta_PackageRemovedThenReAdded", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + auto p3 = MakePackage("Publisher3.Id", "Package 3"); + + DeltaTestContext context{ { p1, p2, p3 } }; + + rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), "Publisher2.Id").value(); + + // Removing the middle package frees its rowid, but the next insert takes one above the highest + // in use, so the re-add cannot land back on it. + context.Remove(p2); + context.Add(p2); + + context.GenerateDelta(); + + rowid_t newRowId = GetPreparedPackageRowId(context.WorkingFile.GetPath(), "Publisher2.Id").value(); + REQUIRE(newRowId != originalRowId); + + { + Connection delta = context.OpenDeltaConnection(); + + // The old rowid must be vacated, or nothing suppresses the baseline row. + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [rowid] = " + std::to_string(originalRowId) + " AND [is_removed] = 1") == 1); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [rowid] = " + std::to_string(newRowId) + " AND [is_removed] = 0") == 1); + } + + SQLiteIndex combined = context.OpenCombined(); + + SearchRequest request; + request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "Publisher2.Id")); + + // Exactly one, and from the delta rather than the stale baseline row. + auto results = combined.Search(request); + REQUIRE(results.Matches.size() == 1); + + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher2.Id", "Publisher3.Id" }); +} + +// B2. Removing the package that holds the highest rowid frees it for the next package added, so +// a removal and a change can name the same rowid. Writing both violates the delta's primary key. +TEST_CASE("SQLiteIndex_Delta_RemovedPackageRowIdReused", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + auto p3 = MakePackage("Publisher3.Id", "Package 3"); + + DeltaTestContext context{ { p1, p2, p3 } }; + + rowid_t reusedRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), "Publisher3.Id").value(); + + auto p4 = MakePackage("Publisher4.Id", "Package 4"); + + context.Remove(p3); + context.Add(p4); + + REQUIRE_NOTHROW(context.GenerateDelta()); + + // The new package took the rowid that the removed one gave up. + REQUIRE(GetPreparedPackageRowId(context.WorkingFile.GetPath(), "Publisher4.Id").value() == reusedRowId); + + { + Connection delta = context.OpenDeltaConnection(); + + // Only the change is recorded. The removal would be redundant, since a delta row at that + // rowid already displaces the baseline row, and it cannot be written in any case. + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [rowid] = " + std::to_string(reusedRowId)) == 1); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [rowid] = " + std::to_string(reusedRowId)) == + std::set{ "Publisher4.Id" }); + } + + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher2.Id", "Publisher4.Id" }); +} + +// B2 continued. The displaced package's associations must go with it. They are diffed against the +// baseline at the shared rowid, so the values belonging to the old occupant are removed. +TEST_CASE("SQLiteIndex_Delta_ReusedRowIdReplacesAssociations", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1", { "keep" }); + auto p2 = MakePackage("Publisher2.Id", "Package 2", { "old1", "old2" }, { "oldcmd" }, {}, { "OLDPC" }); + + DeltaTestContext context{ { p1, p2 } }; + + auto p3 = MakePackage("Publisher3.Id", "Package 3", { "new1" }, { "newcmd" }, {}, { "NEWPC" }); + + context.Remove(p2); + context.Add(p3); + context.GenerateDelta(); + + Connection merged = context.OpenMergedConnection(); + + // Nothing of the old occupant survives at the shared rowid. + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher3.Id") == std::set{ "new1" }); + REQUIRE(GetOneToManyValues(merged, "commands2", "command", "Publisher3.Id") == std::set{ "newcmd" }); + REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", "Publisher3.Id") == std::set{ "newpc" }); + + // The untouched package is unaffected. + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id") == std::set{ "keep" }); +} + +// B6. Remove, re-add, and remove again leaves a tombstone for each rowid the package vacated. They +// all name the same package, and it resolves to one baseline rowid, so it must be reported once. +TEST_CASE("SQLiteIndex_Delta_RemoveAddRemove", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + auto p3 = MakePackage("Publisher3.Id", "Package 3"); + + DeltaTestContext context{ { p1, p2, p3 } }; + + rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), "Publisher2.Id").value(); + + context.Remove(p2); + context.Add(p2); + context.Remove(p2); + + { + Connection working = Connection::Create(context.WorkingFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + + // One tombstone per vacated rowid, but a single identifier reported to generation. + REQUIRE(GetScalar(working, "SELECT COUNT(*) FROM [update_tracking] WHERE [package] = 'Publisher2.Id' AND [is_removed] = 1") == 2); + + auto removals = Tracking::GetRemovalsSince(working, 0, Tracking::RemovalBehavior::Record); + REQUIRE(std::count(removals.begin(), removals.end(), "Publisher2.Id") == 1); + } + + REQUIRE_NOTHROW(context.GenerateDelta()); + + { + Connection delta = context.OpenDeltaConnection(); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'Publisher2.Id'") == 1); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [rowid] = " + std::to_string(originalRowId) + " AND [is_removed] = 1") == 1); + } + + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher3.Id" }); +} + +// B7. When the re-add happens to land on the rowid the package just gave up, the tracking row is +// updated in place. Leaving a tombstone next to the live row would contradict it. +TEST_CASE("SQLiteIndex_Delta_ReAddOnSameRowIdUpdatesInPlace", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + + DeltaTestContext context{ { p1, p2 } }; + + rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), "Publisher2.Id").value(); + + // Publisher2 holds the highest rowid, so removing it frees the value that the re-add takes. + context.Remove(p2); + context.Add(p2); + + context.GenerateDelta(); + + REQUIRE(GetPreparedPackageRowId(context.WorkingFile.GetPath(), "Publisher2.Id").value() == originalRowId); + + Connection delta = context.OpenDeltaConnection(); + + // A single live row, and no tombstone left behind to contradict it. + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'Publisher2.Id'") == 1); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'Publisher2.Id' AND [is_removed] = 0") == 1); + + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher2.Id" }); +} + +// B8. Two identifiers legitimately share a rowid in the tracking table when one vacates it and the +// other takes it. A unique index on the rowid alone would reject exactly the case that the +// tombstone exists to record; only the live rows are constrained. +TEST_CASE("SQLiteIndex_Delta_TrackingAllowsSharedRowIdAcrossPackages", "[sqliteindex][V2_1][updatetracking]") +{ + TempFile indexFile{ "updatetracking"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, s_DeltaVersion); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.RemoveManifest(m1.Manifest, m1.Path); + + // Publisher1 gave up the only rowid in use, so Publisher2 takes it. + REQUIRE_NOTHROW(index.AddManifest(m2.Manifest, m2.Path)); + REQUIRE(index.CheckConsistency(true)); + } + + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadOnly); + + int64_t sharedRowId = GetScalar(connection, "SELECT [package_rowid] FROM [update_tracking] WHERE [package] = 'Publisher2.Id'"); + REQUIRE(GetScalar(connection, "SELECT [package_rowid] FROM [update_tracking] WHERE [package] = 'Publisher1.Id'") == sharedRowId); + + REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [package_rowid] = " + std::to_string(sharedRowId)) == 2); + REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [package_rowid] = " + std::to_string(sharedRowId) + " AND [is_removed] = 0") == 1); +} + +// B9. Identifiers that differ only by case are the same package. The index decides that with the +// ICU LIKE implementation, which folds beyond ASCII, so a NOCASE index would disagree here. +TEST_CASE("SQLiteIndex_Delta_TrackingIdentityIsIcuCaseInsensitive", "[sqliteindex][V2_1][updatetracking]") +{ + TempFile indexFile{ "updatetracking"s, ".db"s }; + + // Cyrillic, where the case mapping is well defined but outside the ASCII range that the + // built in NOCASE collation folds. + ManifestAndPath lower; + CreateFakeManifestAndPath(lower, "\xd0\xbf\xd1\x80\xd0\xb8\xd0\xbc\xd0\xb5\xd1\x80", "1.0"); + ManifestAndPath upper; + CreateFakeManifestAndPath(upper, "\xd0\x9f\xd0\xa0\xd0\x98\xd0\x9c\xd0\x95\xd0\xa0", "2.0"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, s_DeltaVersion); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(lower.Manifest, lower.Path); + index.AddManifest(upper.Manifest, upper.Path); + REQUIRE(index.CheckConsistency(true)); + } + + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadOnly); + + // One package, so one live tracking row. Two would mean the two spellings were treated as + // different packages, and the live row constraint failed to see them as one. + REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [is_removed] = 0") == 1); + REQUIRE(GetScalar(connection, "SELECT COUNT(DISTINCT [package_rowid]) FROM [update_tracking]") == 1); +} + +// B10. The rowid the tracking table stores has to be the one the prepared index assigns, since +// that is what generation and the merged views agree on. This also fails if pinning regresses. +TEST_CASE("SQLiteIndex_Delta_TrackingRowIdMatchesPreparedIndex", "[sqliteindex][V2_1][updatetracking]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + auto p3 = MakePackage("Publisher3.Id", "Package 3"); + + DeltaTestContext context{ { p1, p2, p3 } }; + + std::vector> tracked; + + { + Connection working = Connection::Create(context.WorkingFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + Statement statement = Statement::Create(working, "SELECT [package], [package_rowid] FROM [update_tracking] WHERE [is_removed] = 0"); + + while (statement.Step()) + { + tracked.emplace_back(statement.GetColumn(0), statement.GetColumn(1)); + } + } + + REQUIRE(tracked.size() == 3); + + for (const auto& [packageId, trackedRowId] : tracked) + { + INFO(packageId); + auto preparedRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), packageId); + REQUIRE(preparedRowId.has_value()); + REQUIRE(preparedRowId.value() == trackedRowId); + } +} + +// B11. The migration adds the rowid column to a table whose rows predate it. A backfill that left +// nulls behind would break the first delta generated after an upgrade. +TEST_CASE("SQLiteIndex_Delta_TrackingMigrationBackfillsRowIds", "[sqliteindex][V2_1][updatetracking]") +{ + TempFile indexFile{ "updatetracking"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 0 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + } + + { + SQLiteIndex index = SQLiteIndex::Open(indexFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + REQUIRE(index.MigrateTo(s_DeltaVersion)); + REQUIRE(index.CheckConsistency(true)); + } + + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadOnly); + + REQUIRE(GetRowCount(connection, "update_tracking") == 2); + + // The column arrives with a default of 0, so an unbackfilled row is 0 rather than null. + REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [package_rowid] = 0") == 0); + + // The backfilled value has to be the one the index itself uses, not just any non null. + for (std::string_view packageId : { "Publisher1.Id"sv, "Publisher2.Id"sv }) + { + INFO(packageId); + int64_t tracked = GetScalar(connection, "SELECT [package_rowid] FROM [update_tracking] WHERE [package] = '" + std::string{ packageId } + "'"); + auto idRowId = Schema::V1_0::IdTable::SelectIdByValue(connection, std::string{ packageId }); + REQUIRE(idRowId.has_value()); + REQUIRE(idRowId.value() == tracked); + } +} + +// B3/B4. Rowids have to survive repeated preparation, not just one round. Two rounds cannot +// distinguish a stable assignment from one that happens to repeat. +TEST_CASE("SQLiteIndex_Delta_RowIdsAreStableAcrossPrepares", "[sqliteindex][V2_0]") +{ + TempFile workingFile{ "rowid_working"s, ".db"s }; + + std::vector manifests(5); + CreateFakeManifestAndPath(manifests[0], "Publisher1", "1.0"); + CreateFakeManifestAndPath(manifests[1], "Publisher2", "1.0"); + CreateFakeManifestAndPath(manifests[2], "Publisher3", "1.0"); + CreateFakeManifestAndPath(manifests[3], "Publisher4", "1.0"); + CreateFakeManifestAndPath(manifests[4], "Publisher5", "1.0"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(workingFile, SQLiteVersion{ 2, 0 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(manifests[0].Manifest, manifests[0].Path); + index.AddManifest(manifests[1].Manifest, manifests[1].Path); + index.AddManifest(manifests[2].Manifest, manifests[2].Path); + } + + auto prepareCopy = [&](const TempFile& target) + { + std::filesystem::copy_file(workingFile.GetPath(), target.GetPath(), std::filesystem::copy_options::overwrite_existing); + SQLiteIndex prepared = SQLiteIndex::Open(target.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + prepared.PrepareForPackaging(); + }; + + TempFile first{ "rowid_first"s, ".db"s }; + prepareCopy(first); + + // Round two: drop one package and add another. + { + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.RemoveManifest(manifests[0].Manifest, manifests[0].Path); + index.AddManifest(manifests[3].Manifest, manifests[3].Path); + } + + TempFile second{ "rowid_second"s, ".db"s }; + prepareCopy(second); + + // Round three, to catch an assignment that only appears stable over a single step. + { + SQLiteIndex index = SQLiteIndex::Open(workingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.RemoveManifest(manifests[1].Manifest, manifests[1].Path); + index.AddManifest(manifests[4].Manifest, manifests[4].Path); + } + + TempFile third{ "rowid_third"s, ".db"s }; + prepareCopy(third); + + // Publisher3 is present throughout and must never move. + rowid_t p3First = GetPreparedPackageRowId(first.GetPath(), "Publisher3.Id").value(); + REQUIRE(GetPreparedPackageRowId(second.GetPath(), "Publisher3.Id").value() == p3First); + REQUIRE(GetPreparedPackageRowId(third.GetPath(), "Publisher3.Id").value() == p3First); + + // Publisher4 survives from round two to round three. + REQUIRE(GetPreparedPackageRowId(third.GetPath(), "Publisher4.Id").value() == GetPreparedPackageRowId(second.GetPath(), "Publisher4.Id").value()); + + // B5. A package added after the baseline must land above everything the baseline holds, or it + // would collide with an untouched package when the two are merged. + rowid_t maxInFirst = 0; + for (std::string_view id : { "Publisher1.Id"sv, "Publisher2.Id"sv, "Publisher3.Id"sv }) + { + maxInFirst = std::max(maxInFirst, GetPreparedPackageRowId(first.GetPath(), id).value()); + } + + REQUIRE(GetPreparedPackageRowId(second.GetPath(), "Publisher4.Id").value() > maxInFirst); +} + +// --------------------------------------------------------------------------------------------- +// Group C - generation of the packages table +// --------------------------------------------------------------------------------------------- + +TEST_CASE("SQLiteIndex_Delta_AddedPackage", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + + context.Add(MakePackage("Publisher2.Id", "Package 2")); + context.GenerateDelta(); + + Connection delta = context.OpenDeltaConnection(); + + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [is_removed] = 0") == 1); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 0") == std::set{ "Publisher2.Id" }); +} + +TEST_CASE("SQLiteIndex_Delta_RemovedPackage", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + + DeltaTestContext context{ { p1, p2 } }; + + context.Remove(p2); + context.GenerateDelta(); + + Connection delta = context.OpenDeltaConnection(); + + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [is_removed] = 1") == 1); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "Publisher2.Id" }); + + // A removal carries no data beyond identity, so the rest of the row stays null. + REQUIRE(GetScalar(delta, + "SELECT COUNT(*) FROM [delta_packages] WHERE [is_removed] = 1 AND [name] IS NULL AND [latest_version] IS NULL AND [hash] IS NULL") == 1); +} + +// C4. Only the identifier and name have ever been asserted, so a column dropped from the copy +// would go unnoticed. +TEST_CASE("SQLiteIndex_Delta_ChangedPackageCopiesEveryColumn", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + + { + SQLiteIndex index = context.OpenWorkingForChanges(); + + ManifestAndPath added; + CreateFakeManifestAndPath(added, "Publisher2", "3.4.5", "1.2"sv, "6.7"sv); + index.AddManifest(added.Manifest, added.Path); + } + + context.GenerateDelta(); + + Connection delta = context.OpenDeltaConnection(); + Connection source = Connection::Create(context.WorkingFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + + for (std::string_view column : { "id"sv, "name"sv, "moniker"sv, "latest_version"sv, "arp_min_version"sv, "arp_max_version"sv }) + { + INFO(column); + + auto fromDelta = GetStrings(delta, "SELECT [" + std::string{ column } + "] FROM [delta_packages] WHERE [id] = 'Publisher2.Id'"); + auto fromSource = GetStrings(source, "SELECT [" + std::string{ column } + "] FROM [packages] WHERE [id] = 'Publisher2.Id'"); + + REQUIRE(fromDelta == fromSource); + REQUIRE(fromDelta.size() == 1); + REQUIRE(!fromDelta.begin()->empty()); + } + + // The hash is a blob, so compare it as one rather than through the string accessor. + Statement deltaHash = Statement::Create(delta, "SELECT [hash] FROM [delta_packages] WHERE [id] = 'Publisher2.Id'"); + REQUIRE(deltaHash.Step()); + Statement sourceHash = Statement::Create(source, "SELECT [hash] FROM [packages] WHERE [id] = 'Publisher2.Id'"); + REQUIRE(sourceHash.Step()); + + auto hashValue = deltaHash.GetColumn(0); + REQUIRE(!hashValue.empty()); + REQUIRE(hashValue == sourceHash.GetColumn(0)); +} + +TEST_CASE("SQLiteIndex_Delta_MultipleChangesAndRemovals", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + auto p3 = MakePackage("Publisher3.Id", "Package 3"); + auto p4 = MakePackage("Publisher4.Id", "Package 4"); + + DeltaTestContext context{ { p1, p2, p3, p4 } }; + + context.Remove(p1); + context.Remove(p2); + context.Update(MakePackage("Publisher3.Id", "Renamed 3")); + context.Add(MakePackage("Publisher5.Id", "Package 5")); + context.Add(MakePackage("Publisher6.Id", "Package 6")); + + context.GenerateDelta(); + + Connection delta = context.OpenDeltaConnection(); + + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == + std::set{ "Publisher1.Id", "Publisher2.Id" }); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 0") == + std::set{ "Publisher3.Id", "Publisher5.Id", "Publisher6.Id" }); + + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher3.Id", "Publisher4.Id", "Publisher5.Id", "Publisher6.Id" }); +} + +// C6. A package that came and went within the window was never in the baseline, so there is +// nothing to suppress and the delta must not claim to remove it. +TEST_CASE("SQLiteIndex_Delta_PackageAddedAndRemovedWithinWindow", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + + auto transient = MakePackage("Transient.Id", "Transient"); + context.Add(transient); + context.Remove(transient); + + REQUIRE_NOTHROW(context.GenerateDelta()); + + Connection delta = context.OpenDeltaConnection(); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'Transient.Id'") == 0); + + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id" }); +} + +// C8. The identifier lookup used to be a LIKE, which treats these characters as wildcards. A +// package whose identifier contains them would have matched the wrong row. +TEST_CASE("SQLiteIndex_Delta_IdentifierWithLikeWildcards", "[sqliteindex][V2_1][delta]") +{ + auto wild = MakePackage("Publisher_A.Id", "Wildcard Underscore"); + auto decoy = MakePackage("PublisherXA.Id", "Decoy"); + auto percent = MakePackage("Pub%cent.Id", "Wildcard Percent"); + + DeltaTestContext context{ { wild, decoy, percent } }; + + context.Remove(wild); + context.GenerateDelta(); + + Connection delta = context.OpenDeltaConnection(); + + // Only the package actually removed; a LIKE would also have matched the decoy. + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "Publisher_A.Id" }); + + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(GetSearchedIds(combined) == std::set{ "PublisherXA.Id", "Pub%cent.Id" }); +} + +TEST_CASE("SQLiteIndex_Delta_NoChanges_EmptyDelta", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + + // Reset the base time without making any change, so nothing is reported. + context.OpenWorkingForChanges(); + context.GenerateDelta(); + + Connection delta = context.OpenDeltaConnection(); + + // The delta is a database in its own right, carrying the metadata that any index has. Without + // it the delta could not be opened, since opening reads the schema version to pick an interface. + REQUIRE(MetadataTable::GetNamedValue(delta, s_MetadataValueName_MajorVersion) == 2); + REQUIRE(MetadataTable::GetNamedValue(delta, s_MetadataValueName_MinorVersion) == 1); + REQUIRE(!MetadataTable::TryGetNamedValue(delta, s_MetadataValueName_DatabaseIdentifier).value_or(std::string{}).empty()); + REQUIRE(MetadataTable::TryGetNamedValue(delta, s_MetadataValueName_LastWriteTime).has_value()); + + REQUIRE(GetRowCount(delta, "delta_packages") == 0); + + for (const auto& table : Delta::SystemReferenceTables()) + { + INFO(table.TableName); + REQUIRE(GetRowCount(delta, Delta::GetTableName(table.TableName)) == 0); + } + + for (const auto& table : Delta::OneToManyTables()) + { + INFO(table.TableName); + REQUIRE(GetRowCount(delta, Delta::GetTableName(table.TableName)) == 0); + REQUIRE(GetRowCount(delta, Delta::GetMapTableName(table.TableName)) == 0); + } + + // J4. The delta ships as plain tables, just as a prepared 2.0 index does. Its indexes serve + // only generation, and the merged views probe nothing but primary keys. + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [sqlite_master] WHERE [type] = 'index'") == 0); + + // An empty delta still has to merge cleanly. + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id" }); +} + +// --------------------------------------------------------------------------------------------- +// Group D - generation of the system reference tables +// +// These decide whether ARP correlation still works after an update, and nothing covered them. +// --------------------------------------------------------------------------------------------- + +TEST_CASE("SQLiteIndex_Delta_SystemReference_AddAndRemove", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, { "Family1_8wekyb3d8bbwe" }, { "PC-KEEP", "PC-DROP" }); + auto p2 = MakePackage("Publisher2.Id", "Package 2", { "t1" }, { "c1" }, { "Family2_8wekyb3d8bbwe" }, { "PC-OTHER" }); + + DeltaTestContext context{ { p1, p2 } }; + + // Trade one product code for another while keeping a third, and swap the family name. + context.Update(MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, { "Family3_8wekyb3d8bbwe" }, { "PC-KEEP", "PC-NEW" })); + + context.GenerateDelta(); + + { + Connection delta = context.OpenDeltaConnection(); + + // D6. Only the difference is recorded. Writing the whole current set would destroy the size + // benefit that justifies the feature. + auto productCodeTable = Delta::GetTableName("productcodes2"); + REQUIRE(GetStrings(delta, "SELECT [productcode] FROM [" + productCodeTable + "] WHERE [is_removed] = 0") == + std::set{ "pc-new" }); + REQUIRE(GetStrings(delta, "SELECT [productcode] FROM [" + productCodeTable + "] WHERE [is_removed] = 1") == + std::set{ "pc-drop" }); + + // The kept value is not mentioned at all. + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [" + productCodeTable + "] WHERE [productcode] = 'pc-keep'") == 0); + } + + Connection merged = context.OpenMergedConnection(); + + // D3. Suppression is per row: the kept code survives even though the package changed. + REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", "Publisher1.Id") == + std::set{ "pc-keep", "pc-new" }); + REQUIRE(GetSystemReferenceValues(merged, "pfns2", "pfn", "Publisher1.Id") == + std::set{ "family3_8wekyb3d8bbwe" }); + + // The untouched package keeps everything. + REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", "Publisher2.Id") == + std::set{ "pc-other" }); +} + +// D7. The user visible consequence: correlation by product code has to find the updated package. +TEST_CASE("SQLiteIndex_Delta_SystemReference_CorrelationThroughCombined", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, { "Family1_8wekyb3d8bbwe" }, { "PC-KEEP", "PC-DROP" }); + + DeltaTestContext context{ { p1, MakePackage("Publisher2.Id", "Package 2") } }; + + context.Update(MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, { "Family1_8wekyb3d8bbwe" }, { "PC-KEEP", "PC-NEW" })); + context.GenerateDelta(); + + SQLiteIndex combined = context.OpenCombined(); + + for (std::string_view productCode : { "PC-KEEP"sv, "PC-NEW"sv }) + { + INFO(productCode); + + SearchRequest request; + request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, std::string{ productCode })); + + REQUIRE(GetSearchedIds(combined, request) == std::set{ "Publisher1.Id" }); + } + + // The dropped code must no longer correlate to anything. + SearchRequest dropped; + dropped.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, "PC-DROP"s)); + REQUIRE(combined.Search(dropped).Matches.empty()); + + // And the family name still resolves through the merged view. + SearchRequest family; + family.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::PackageFamilyName, MatchType::Exact, "Family1_8wekyb3d8bbwe"s)); + REQUIRE(GetSearchedIds(combined, family) == std::set{ "Publisher1.Id" }); +} + +// D5. The normalized name and publisher tables are populated as a side effect of the package name, +// so a rename has to move them. Nothing asserted on them before. +TEST_CASE("SQLiteIndex_Delta_SystemReference_NormalizedNameFollowsRename", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Original Name") } }; + + context.Update(MakePackage("Publisher1.Id", "Replacement Name")); + context.GenerateDelta(); + + Connection merged = context.OpenMergedConnection(); + + auto names = GetSystemReferenceValues(merged, "norm_names2", "norm_name", "Publisher1.Id"); + REQUIRE(names.size() == 1); + + // The old name must be gone rather than merely joined by the new one. + for (const auto& name : names) + { + REQUIRE(name.find("original") == std::string::npos); + } + + // Rather than predicting what normalization produces, require that the merged form holds + // exactly what a full index built from the same data holds. + Connection reference = Connection::Create(context.WorkingFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + + REQUIRE(GetSystemReferenceValues(merged, "norm_names2", "norm_name", "Publisher1.Id") == + GetSystemReferenceValues(reference, "norm_names2", "norm_name", "Publisher1.Id")); + + REQUIRE(GetSystemReferenceValues(merged, "norm_publishers2", "norm_publisher", "Publisher1.Id") == + GetSystemReferenceValues(reference, "norm_publishers2", "norm_publisher", "Publisher1.Id")); +} + +TEST_CASE("SQLiteIndex_Delta_SystemReference_RemovedPackageValuesAreInvisible", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, { "Family1_8wekyb3d8bbwe" }, { "PC-1" }); + auto p2 = MakePackage("Publisher2.Id", "Package 2", { "t1" }, { "c1" }, { "Family2_8wekyb3d8bbwe" }, { "PC-2" }); + + DeltaTestContext context{ { p1, p2 } }; + + context.Remove(p2); + context.GenerateDelta(); + + { + Connection delta = context.OpenDeltaConnection(); + + // F3. A removal is recorded once, against the package. There are no per value tombstones, + // so the package row is the only thing that can suppress these. + REQUIRE(GetRowCount(delta, Delta::GetTableName("productcodes2")) == 0); + REQUIRE(GetRowCount(delta, Delta::GetTableName("pfns2")) == 0); + } + + Connection merged = context.OpenMergedConnection(); + + REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", "Publisher2.Id").empty()); + REQUIRE(GetSystemReferenceValues(merged, "pfns2", "pfn", "Publisher2.Id").empty()); + REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", "Publisher1.Id") == std::set{ "pc-1" }); + + SQLiteIndex combined = context.OpenCombined(); + + SearchRequest request; + request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, "PC-2"s)); + REQUIRE(combined.Search(request).Matches.empty()); +} + +// --------------------------------------------------------------------------------------------- +// Group E - generation of the one to many tables +// --------------------------------------------------------------------------------------------- + +// E1. A package that trades one tag for another while keeping a third. Suppressing the baseline +// at the level of the package would lose the kept tag, because the delta never mentions it. +TEST_CASE("SQLiteIndex_Delta_OneToMany_AssociationsAreSuppressedPerRow", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1", { "keep", "drop", "alsokeep" }, { "cmdkeep", "cmddrop" }); + auto p2 = MakePackage("Publisher2.Id", "Package 2", { "other" }, { "othercmd" }); + + DeltaTestContext context{ { p1, p2 } }; + + context.Update(MakePackage("Publisher1.Id", "Package 1", { "keep", "added", "alsokeep" }, { "cmdkeep", "cmdadded" })); + context.Remove(p2); + + context.GenerateDelta(); + + Connection merged = context.OpenMergedConnection(); + + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id") == std::set{ "keep", "added", "alsokeep" }); + + // E2. The same defect on the other one to many table. + REQUIRE(GetOneToManyValues(merged, "commands2", "command", "Publisher1.Id") == std::set{ "cmdkeep", "cmdadded" }); + + // F3 again, for the map tables. + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher2.Id").empty()); + REQUIRE(GetOneToManyValues(merged, "commands2", "command", "Publisher2.Id").empty()); +} + +// E3/E4. A value the baseline already knows is referenced at its existing rowid rather than copied, +// and a genuinely new value is numbered above everything the baseline holds. +TEST_CASE("SQLiteIndex_Delta_OneToMany_ValueRowIdAllocation", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1", { "shared", "only1" }); + auto p2 = MakePackage("Publisher2.Id", "Package 2", { "other" }); + + DeltaTestContext context{ { p1, p2 } }; + + // Publisher2 gains a tag the baseline already has, plus one it does not. + context.Update(MakePackage("Publisher2.Id", "Package 2", { "other", "shared", "brandnew" })); + context.GenerateDelta(); + + rowid_t baselineMaxTagRowId = 0; + rowid_t sharedRowIdInBaseline = 0; + + { + Connection baseline = Connection::Create(context.BaselineFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + baselineMaxTagRowId = static_cast(GetScalar(baseline, "SELECT MAX([rowid]) FROM [tags2]")); + sharedRowIdInBaseline = static_cast(GetScalar(baseline, "SELECT [rowid] FROM [tags2] WHERE [tag] = 'shared'")); + } + + Connection delta = context.OpenDeltaConnection(); + + // E4. The shared value is not copied into the delta; the map points at the baseline rowid. + REQUIRE(GetStrings(delta, "SELECT [tag] FROM [delta_tags2]") == std::set{ "brandnew" }); + + auto mapTable = Delta::GetMapTableName("tags2"); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [" + mapTable + "] WHERE [tag] = " + std::to_string(sharedRowIdInBaseline) + " AND [is_removed] = 0") == 1); + + // E3. The new value is numbered above the baseline, so the union cannot collide. + REQUIRE(GetScalar(delta, "SELECT [rowid] FROM [delta_tags2] WHERE [tag] = 'brandnew'") > baselineMaxTagRowId); + + // F4. Both sides resolve through the unioned value view. + Connection merged = context.OpenMergedConnection(); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher2.Id") == std::set{ "other", "shared", "brandnew" }); +} + +// E5. One new value shared by two packages is stored once and mapped twice. +TEST_CASE("SQLiteIndex_Delta_OneToMany_NewValueSharedByPackages", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1" }); + auto p2 = MakePackage("Publisher2.Id", "Package 2", { "t2" }); + + DeltaTestContext context{ { p1, p2 } }; + + context.Update(MakePackage("Publisher1.Id", "Package 1", { "t1", "commontag" })); + context.Update(MakePackage("Publisher2.Id", "Package 2", { "t2", "commontag" })); + + context.GenerateDelta(); + + Connection delta = context.OpenDeltaConnection(); + + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_tags2] WHERE [tag] = 'commontag'") == 1); + + rowid_t valueRowId = static_cast(GetScalar(delta, "SELECT [rowid] FROM [delta_tags2] WHERE [tag] = 'commontag'")); + auto mapTable = Delta::GetMapTableName("tags2"); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [" + mapTable + "] WHERE [tag] = " + std::to_string(valueRowId) + " AND [is_removed] = 0") == 2); + + Connection merged = context.OpenMergedConnection(); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id") == std::set{ "t1", "commontag" }); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher2.Id") == std::set{ "t2", "commontag" }); +} + +// E6. Every value removed from a package. An empty current set must not read as "nothing changed". +TEST_CASE("SQLiteIndex_Delta_OneToMany_AllValuesRemoved", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1", "t2" }, { "c1" }); + + DeltaTestContext context{ { p1, MakePackage("Publisher2.Id", "Package 2", { "t3" }, { "c2" }) } }; + + context.Update(MakePackage("Publisher1.Id", "Package 1", {}, {})); + context.GenerateDelta(); + + { + Connection delta = context.OpenDeltaConnection(); + auto mapTable = Delta::GetMapTableName("tags2"); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [" + mapTable + "] WHERE [is_removed] = 1") == 2); + } + + Connection merged = context.OpenMergedConnection(); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id").empty()); + REQUIRE(GetOneToManyValues(merged, "commands2", "command", "Publisher1.Id").empty()); + + // The other package is untouched. + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher2.Id") == std::set{ "t3" }); +} + +// E7. A baseline with no values at all, so the maximum rowid query has nothing to report. +TEST_CASE("SQLiteIndex_Delta_OneToMany_EmptyBaselineValueTable", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1", {}, {}) } }; + + { + Connection baseline = Connection::Create(context.BaselineFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + REQUIRE(GetRowCount(baseline, "tags2") == 0); + } + + context.Update(MakePackage("Publisher1.Id", "Package 1", { "first" }, { "firstcmd" })); + REQUIRE_NOTHROW(context.GenerateDelta()); + + Connection delta = context.OpenDeltaConnection(); + + // Numbering has to start somewhere valid; rowid 0 is not. + REQUIRE(GetScalar(delta, "SELECT [rowid] FROM [delta_tags2] WHERE [tag] = 'first'") > 0); + + Connection merged = context.OpenMergedConnection(); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id") == std::set{ "first" }); +} + +// --------------------------------------------------------------------------------------------- +// Group F - merged views +// --------------------------------------------------------------------------------------------- + +// F5. In delta read mode the packages table is a view, so the check that decides whether an index +// has been prepared cannot rely on the table existing. Without this the 1.7 internal interface +// would be built over the views. +TEST_CASE("SQLiteIndex_Delta_CombinedIndexIsInPreparedState", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + + context.Add(MakePackage("Publisher2.Id", "Package 2")); + context.GenerateDelta(); + + SQLiteIndex combined = context.OpenCombined(); + + // A prepared index reports no manifest paths; reaching the 1.7 interface would change that, + // and would more likely fail outright looking for tables that are not there. + auto results = combined.Search({}); + REQUIRE(results.Matches.size() == 2); + + REQUIRE(combined.GetPropertyByPrimaryId(results.Matches[0].first, PackageVersionProperty::Id).has_value()); +} + +// --------------------------------------------------------------------------------------------- +// Group G - combined open, affinity, and the negative paths +// --------------------------------------------------------------------------------------------- + +TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_Search", "[sqliteindex][V2_1][delta]") +{ + // Immutable is the disposition the shipped path uses, and it is the one that reaches SQLite as + // a URI rather than a plain path, so both files have to be named that way for the attach to + // resolve at all. + auto disposition = GENERATE(SQLiteStorageBase::OpenDisposition::Read, SQLiteStorageBase::OpenDisposition::Immutable); + + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + + context.Add(MakePackage("Publisher2.Id", "Package 2")); + context.GenerateDelta(); + + SQLiteIndex combined = context.OpenCombined(disposition); + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher2.Id" }); +} + +TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_RemovedPackageExcluded", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + + DeltaTestContext context{ { p1, p2 } }; + + context.Remove(p2); + context.GenerateDelta(); + + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id" }); +} + +TEST_CASE("SQLiteIndex_Delta_UnmarkedBaselineRejected", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context; + context.CreateWorking({ MakePackage("Publisher1.Id", "Package 1") }); + context.CaptureBaseline(false); + + context.Add(MakePackage("Publisher2.Id", "Package 2")); + + // Without a designation the baseline has no identity, so nothing could tie the delta to it. + REQUIRE_THROWS_HR(context.GenerateDelta(), APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); +} + +TEST_CASE("SQLiteIndex_Delta_MismatchedBaselineRejected", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + + // A second baseline with byte identical contents. Designating each gives it its own identity, + // which is the point: a delta is tied to the baseline it was computed from, not to data that + // happens to look like it. + TempFile otherBaselineFile{ "delta_baseline_other"s, ".db"s }; + std::filesystem::copy_file(context.WorkingFile.GetPath(), otherBaselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); + + { + SQLiteIndex other = SQLiteIndex::Open(otherBaselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + other.PrepareForPackaging(); + other.MarkAsBaseline(); + } + + context.Add(MakePackage("Publisher2.Id", "Package 2")); + context.GenerateDelta(); + + REQUIRE_NOTHROW(context.OpenCombined()); + + REQUIRE_THROWS_HR( + SQLiteIndex::OpenWithBaseline(context.DeltaFile.GetPath().u8string(), otherBaselineFile.GetPath().u8string()), + APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); +} + +// G9. Designating an index a second time mints a new identity, which invalidates any delta made +// against the first. Designation is deliberately not idempotent. +TEST_CASE("SQLiteIndex_Delta_ReMarkingBaselineInvalidatesDelta", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + + context.Add(MakePackage("Publisher2.Id", "Package 2")); + context.GenerateDelta(); + + REQUIRE_NOTHROW(context.OpenCombined()); + + { + SQLiteIndex baseline = SQLiteIndex::Open(context.BaselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + baseline.MarkAsBaseline(); + } + + REQUIRE_THROWS_HR(context.OpenCombined(), APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); +} + +// G5. The combined form is a set of views over a union, so there is nothing to write back to. +TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_ReadWriteRejected", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + + context.Add(MakePackage("Publisher2.Id", "Package 2")); + context.GenerateDelta(); + + REQUIRE_THROWS_HR(context.OpenCombined(SQLiteStorageBase::OpenDisposition::ReadWrite), E_INVALIDARG); +} + +// G6. A baseline that cannot be read at all has to fail rather than silently produce a delta only +// view of the world. +TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_MissingBaseline", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + + context.Add(MakePackage("Publisher2.Id", "Package 2")); + context.GenerateDelta(); + + TempFile missing{ "delta_missing_baseline"s, ".db"s }; + std::filesystem::remove(missing.GetPath()); + + REQUIRE_THROWS(SQLiteIndex::OpenWithBaseline(context.DeltaFile.GetPath().u8string(), missing.GetPath().u8string())); +} + +// G7/G8. The delta entry points are 2.1 only, and the base implementations say so rather than +// doing something undefined. +TEST_CASE("SQLiteIndex_Delta_NotSupportedBefore_2_1", "[sqliteindex][V2_0][delta]") +{ + TempFile indexFile{ "delta_unsupported"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 0 }); + index.AddManifest(m1.Manifest, m1.Path); + index.PrepareForPackaging(); + + REQUIRE_THROWS_HR(index.MarkAsBaseline(), HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + + // G7. A 2.0 file in the delta position is nonsense, and the interface is what says so. The + // baseline is never touched, so it does not need to exist. + TempFile baselineFile{ "delta_unsupported_baseline"s, ".db"s }; + + REQUIRE_THROWS_HR( + SQLiteIndex::OpenWithBaseline(indexFile.GetPath().u8string(), baselineFile.GetPath().u8string()), + HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); +} + +// --------------------------------------------------------------------------------------------- +// Group I - consistency +// --------------------------------------------------------------------------------------------- + +// I2/I3. A package recorded as removed while still present in the index is a real inconsistency, +// but a package that was removed and re-added is not: it legitimately has both a tombstone and a +// live row. Confusing the two turns a silent bug into a loud but wrong integrity failure. +TEST_CASE("SQLiteIndex_Delta_CheckConsistency_ReAddedPackageIsNotCorruption", "[sqliteindex][V2_1][updatetracking]") +{ + TempFile indexFile{ "updatetracking"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + ManifestAndPath m3; + CreateFakeManifestAndPath(m3, "Publisher3", "1.0"); + + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, s_DeltaVersion); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + index.AddManifest(m3.Manifest, m3.Path); + + // Removing the middle package and re-adding it leaves a tombstone beside a live row. + index.RemoveManifest(m2.Manifest, m2.Path); + index.AddManifest(m2.Manifest, m2.Path); + + REQUIRE(index.CheckConsistency(true)); + + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadOnly); + REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [package] = 'Publisher2.Id'") == 2); + + // The removal is still reported, since the old rowid genuinely was vacated. + auto removals = Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record); + REQUIRE(std::count(removals.begin(), removals.end(), "Publisher2.Id") == 1); + + // And it is not also reported as an update under that identity being gone. + auto updates = Tracking::GetUpdatesSince(connection, 0, Tracking::RemovalBehavior::Record); + REQUIRE(std::count_if(updates.begin(), updates.end(), [](const auto& u) { return u.PackageIdentifier == "Publisher2.Id"; }) == 1); +} + +TEST_CASE("SQLiteIndex_Delta_CheckConsistency_OnCombinedIndex", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, {}, { "PC-1" }); + auto p2 = MakePackage("Publisher2.Id", "Package 2", { "t2" }, { "c2" }, {}, { "PC-2" }); + + DeltaTestContext context{ { p1, p2 } }; + + context.Update(MakePackage("Publisher1.Id", "Package 1", { "t1", "t3" }, { "c1" }, {}, { "PC-1" })); + context.Remove(p2); + context.GenerateDelta(); + + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(combined.CheckConsistency(true)); +} + +// --------------------------------------------------------------------------------------------- +// Group H - equivalence +// +// Enumerating what could go wrong in a merge is unbounded; comparing against a full index built +// from the same data is not. The targeted cases above exist to localize what this detects. +// --------------------------------------------------------------------------------------------- + +namespace +{ + // Compares a combined index against a full index built from the same data. + void RequireEquivalent(const SQLiteIndex& combined, const SQLiteIndex& full) + { + // H1. A battery covering the fields that the merged views are responsible for. + std::vector requests; + + requests.emplace_back(); + + auto addFieldRequest = [&](PackageMatchField field, MatchType type, std::string value, std::string second = {}) + { + SearchRequest request; + if (second.empty()) + { + request.Inclusions.emplace_back(PackageMatchFilter(field, type, std::move(value))); + } + else + { + request.Inclusions.emplace_back(PackageMatchFilter(field, type, std::move(value), std::move(second))); + } + requests.emplace_back(std::move(request)); + }; + + for (MatchType type : { MatchType::Exact, MatchType::Substring, MatchType::StartsWith }) + { + addFieldRequest(PackageMatchField::Id, type, "Equivalence"); + addFieldRequest(PackageMatchField::Name, type, "Package"); + addFieldRequest(PackageMatchField::Moniker, type, "moniker"); + addFieldRequest(PackageMatchField::Tag, type, "shared"); + addFieldRequest(PackageMatchField::Tag, type, "changed"); + addFieldRequest(PackageMatchField::Command, type, "cmd"); + addFieldRequest(PackageMatchField::ProductCode, type, "PC"); + addFieldRequest(PackageMatchField::PackageFamilyName, type, "Family"); + } + + { + SearchRequest query; + query.Query = RequestMatch(MatchType::Substring, "Package"); + requests.emplace_back(std::move(query)); + } + + addFieldRequest(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, "Package Untouched", "Publisher"); + addFieldRequest(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, "Package Replacement", "Publisher"); + addFieldRequest(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, "Package Original", "Publisher"); + + for (size_t i = 0; i < requests.size(); ++i) + { + INFO("request index " << i); + + // A field and match type combination that the index does not support is not a delta + // concern, but the two forms still have to agree about it. + std::optional> combinedIds; + std::optional> fullIds; + + try + { + combinedIds = GetSearchedIds(combined, requests[i]); + } + catch (...) {} + + try + { + fullIds = GetSearchedIds(full, requests[i]); + } + catch (...) {} + + REQUIRE(combinedIds.has_value() == fullIds.has_value()); + REQUIRE(combinedIds == fullIds); + } + + // H3. The identifiers agreeing is not enough; the rowids that name them have to agree too, + // since that is what the merge is built on and what a caller carries around. + std::map combinedPrimaryIds; + for (const auto& match : combined.Search({}).Matches) + { + combinedPrimaryIds[combined.GetPropertyByPrimaryId(match.first, PackageVersionProperty::Id).value()] = match.first; + } + + std::map fullPrimaryIds; + for (const auto& match : full.Search({}).Matches) + { + fullPrimaryIds[full.GetPropertyByPrimaryId(match.first, PackageVersionProperty::Id).value()] = match.first; + } + + REQUIRE(combinedPrimaryIds.size() == fullPrimaryIds.size()); + + for (const auto& [packageId, primaryId] : combinedPrimaryIds) + { + INFO(packageId); + + auto full_itr = fullPrimaryIds.find(packageId); + REQUIRE(full_itr != fullPrimaryIds.end()); + REQUIRE(primaryId == full_itr->second); + + // H2. Found is not the same as correct. + for (PackageVersionProperty property : { + PackageVersionProperty::Id, + PackageVersionProperty::Name, + PackageVersionProperty::Moniker, + PackageVersionProperty::Version }) + { + REQUIRE(combined.GetPropertyByPrimaryId(primaryId, property) == full.GetPropertyByPrimaryId(full_itr->second, property)); + } + + for (PackageVersionMultiProperty property : { + PackageVersionMultiProperty::Tag, + PackageVersionMultiProperty::Command, + PackageVersionMultiProperty::PackageFamilyName, + PackageVersionMultiProperty::ProductCode, + PackageVersionMultiProperty::UpgradeCode, + PackageVersionMultiProperty::Name, + PackageVersionMultiProperty::Publisher }) + { + auto combinedValues = combined.GetMultiPropertyByPrimaryId(primaryId, property); + auto fullValues = full.GetMultiPropertyByPrimaryId(full_itr->second, property); + + std::sort(combinedValues.begin(), combinedValues.end()); + std::sort(fullValues.begin(), fullValues.end()); + + REQUIRE(combinedValues == fullValues); + } + } + } +} + +TEST_CASE("SQLiteIndex_Delta_EquivalenceWithFullIndex", "[sqliteindex][V2_1][delta]") +{ + // The mutation set covers every kind of change the delta has to describe: a package added, one + // removed, one whose values change, one renamed, and one left entirely alone. + auto untouched = MakePackage("Equivalence.Untouched", "Package Untouched", { "shared", "keep" }, { "cmdkeep" }, { "Family0_8wekyb3d8bbwe" }, { "PC-UNTOUCHED" }); + auto removed = MakePackage("Equivalence.Removed", "Package Removed", { "shared", "gone" }, { "cmdgone" }, { "Family1_8wekyb3d8bbwe" }, { "PC-REMOVED" }); + auto retagged = MakePackage("Equivalence.Retagged", "Package Retagged", { "shared", "changed" }, { "cmdold" }, { "Family2_8wekyb3d8bbwe" }, { "PC-OLD", "PC-BOTH" }); + auto renamed = MakePackage("Equivalence.Renamed", "Package Original", { "shared" }, { "cmdkeep" }, { "Family3_8wekyb3d8bbwe" }, { "PC-RENAMED" }); + auto roundTrip = MakePackage("Equivalence.RoundTrip", "Package RoundTrip", { "shared" }, { "cmdkeep" }, {}, { "PC-ROUND" }); + + DeltaTestContext context{ { untouched, removed, retagged, renamed, roundTrip } }; + + context.Remove(removed); + context.Update(MakePackage("Equivalence.Retagged", "Package Retagged", { "shared", "changednew" }, { "cmdnew" }, { "Family2_8wekyb3d8bbwe" }, { "PC-NEW", "PC-BOTH" })); + context.Update(MakePackage("Equivalence.Renamed", "Package Replacement", { "shared" }, { "cmdkeep" }, { "Family3_8wekyb3d8bbwe" }, { "PC-RENAMED" })); + context.Add(MakePackage("Equivalence.Added", "Package Added", { "shared", "brand" }, { "cmdadded" }, { "Family4_8wekyb3d8bbwe" }, { "PC-ADDED" })); + + // A removal and re-add in the same window, which is where rowid identity is hardest. + context.Remove(roundTrip); + context.Add(roundTrip); + + context.GenerateDelta(); + + SQLiteIndex combined = context.OpenCombined(); + SQLiteIndex full = context.OpenFullIndex(); + + REQUIRE(GetSearchedIds(full) == std::set{ + "Equivalence.Untouched", "Equivalence.Retagged", "Equivalence.Renamed", "Equivalence.RoundTrip", "Equivalence.Added" }); + + RequireEquivalent(combined, full); +} + +// H5. The degenerate case, which a consumer must not have to special case. +TEST_CASE("SQLiteIndex_Delta_EquivalenceWithEmptyDelta", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Equivalence.One", "Package One", { "shared" }, { "cmdkeep" }, { "Family0_8wekyb3d8bbwe" }, { "PC-1" }); + auto p2 = MakePackage("Equivalence.Two", "Package Two", { "shared", "changed" }, { "cmd2" }, {}, { "PC-2" }); + + DeltaTestContext context{ { p1, p2 } }; + + context.OpenWorkingForChanges(); + context.GenerateDelta(); + + SQLiteIndex combined = context.OpenCombined(); + SQLiteIndex full = context.OpenFullIndex(); + + RequireEquivalent(combined, full); +} diff --git a/src/AppInstallerCLITests/SQLiteIndexTestCommon.cpp b/src/AppInstallerCLITests/SQLiteIndexTestCommon.cpp new file mode 100644 index 0000000000..e9d9a566aa --- /dev/null +++ b/src/AppInstallerCLITests/SQLiteIndexTestCommon.cpp @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "SQLiteIndexTestCommon.h" +#include "TestCommon.h" + +#include +#include + +#include + +using namespace std::string_literals; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Utility; + +namespace TestCommon +{ + SQLiteIndex CreateTestIndex(const std::string& filePath, std::optional version) + { + // If no specific version requested, then use generator to run against the last 3 versions. + if (!version) + { + SQLiteVersion latestVersion{ 2, 0 }; + SQLiteVersion versionMinus1 = SQLiteVersion{ 1, 7 }; + SQLiteVersion versionMinus2 = SQLiteVersion{ 1, 6 }; + + version = GENERATE_COPY(SQLiteVersion{ versionMinus2 }, SQLiteVersion{ versionMinus1 }, SQLiteVersion{ latestVersion }); + } + + return SQLiteIndex::CreateNew(filePath, version.value()); + } + + std::string GetPathFromManifest(Manifest& manifest) + { + auto publisher = manifest.Id; + AppInstaller::Utility::FindAndReplace(publisher, ".", "/"); + + return AppInstaller::Utility::ToLower(publisher).append("/").append(manifest.Version); + } + + void CreateFakeManifest(Manifest& manifest, string_t publisher, string_t version) + { + manifest.Installers.push_back({}); + manifest.Id = publisher.append(".").append("Id"); + manifest.DefaultLocalization.Add(publisher.append(" Name")); + manifest.Moniker = "testmoniker"; + manifest.Version = version; + manifest.Channel = "test"; + manifest.DefaultLocalization.Add({ "t1", "t2" }); + manifest.Installers[0].Commands = { "test1", "test2" }; + } + + void CreateFakeManifestAndPath( + ManifestAndPath& manifestAndPath, + const string_t& publisher, + std::string_view version, + std::optional arpMinVersion, + std::optional arpMaxVersion) + { + CreateFakeManifest(manifestAndPath.Manifest, publisher, string_t{ version }); + manifestAndPath.Path = ConvertToUTF8(CreateNewGuidNameWString()); + manifestAndPath.Manifest.StreamSha256 = AppInstaller::Utility::SHA256::ComputeHash(manifestAndPath.Path); + + if (arpMinVersion) + { + manifestAndPath.Manifest.Installers[0].BaseInstallerType = InstallerTypeEnum::Exe; + manifestAndPath.Manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifestAndPath.Manifest.Installers[0].AppsAndFeaturesEntries.back().DisplayVersion = arpMinVersion.value(); + } + + if (arpMaxVersion) + { + manifestAndPath.Manifest.Installers[0].BaseInstallerType = InstallerTypeEnum::Exe; + manifestAndPath.Manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifestAndPath.Manifest.Installers[0].AppsAndFeaturesEntries.back().DisplayVersion = arpMaxVersion.value(); + } + } + + void ApplyIndexFields(Manifest& manifest, const IndexFields& fields) + { + manifest.Id = fields.Id; + manifest.DefaultLocalization.Add(fields.Name); + manifest.DefaultLocalization.Add(fields.Publisher); + manifest.Moniker = fields.Moniker; + manifest.Version = fields.Version; + manifest.DefaultLocalization.Add(fields.Tags); + + manifest.Installers.resize(std::max(fields.PackageFamilyNames.size(), fields.ProductCodes.size())); + + if (manifest.Installers.size() == 0) + { + manifest.Installers.push_back({}); + } + + manifest.Channel = fields.Channel; + manifest.Installers[0].Commands = fields.Commands; + + for (size_t i = 0; i < fields.PackageFamilyNames.size(); ++i) + { + manifest.Installers[i].PackageFamilyName = fields.PackageFamilyNames[i]; + } + + for (size_t i = 0; i < fields.ProductCodes.size(); ++i) + { + manifest.Installers[i].ProductCode = fields.ProductCodes[i]; + } + + if (!fields.ArpName.empty() || !fields.ArpPublisher.empty()) + { + manifest.Installers[0].AppsAndFeaturesEntries.push_back({}); + manifest.Installers[0].AppsAndFeaturesEntries[0].DisplayName = fields.ArpName; + manifest.Installers[0].AppsAndFeaturesEntries[0].Publisher = fields.ArpPublisher; + } + } + + Manifest CreateManifest(const IndexFields& fields) + { + Manifest manifest; + ApplyIndexFields(manifest, fields); + return manifest; + } + + SQLiteIndex SearchTestSetup(const std::string& filePath, std::initializer_list data, std::optional version) + { + SQLiteIndex index = CreateTestIndex(filePath, version); + + // A single manifest is carried across the whole set, matching what this helper has always + // done. Anything the fields do not overwrite therefore persists from the previous entry. + Manifest manifest; + + for (const auto& d : data) + { + ApplyIndexFields(manifest, d); + index.AddManifest(manifest, d.Path); + } + + return index; + } +} diff --git a/src/AppInstallerCLITests/SQLiteIndexTestCommon.h b/src/AppInstallerCLITests/SQLiteIndexTestCommon.h new file mode 100644 index 0000000000..7d50093739 --- /dev/null +++ b/src/AppInstallerCLITests/SQLiteIndexTestCommon.h @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include +#include + +#include +#include +#include +#include +#include + + +// Fixture helpers shared by the index test files. They live here rather than in TestCommon so that +// only the tests that build indexes pay for the manifest and index headers. +namespace TestCommon +{ + using SQLiteVersion = AppInstaller::SQLite::Version; + + // Creates an index to test against. + // When no version is given, the test is generated once for each of the last few schema + // versions, so callers that care about a specific version must name it. + AppInstaller::Repository::Microsoft::SQLiteIndex CreateTestIndex(const std::string& filePath, std::optional version = {}); + + // Gets the relative path that the given manifest would be stored at. + std::string GetPathFromManifest(AppInstaller::Manifest::Manifest& manifest); + + // Fills in a manifest with values derived from the given publisher. + void CreateFakeManifest( + AppInstaller::Manifest::Manifest& manifest, + AppInstaller::Manifest::string_t publisher, + AppInstaller::Manifest::string_t version = "1.0.0"); + + // A manifest along with the path that it is stored at. + struct ManifestAndPath + { + AppInstaller::Manifest::Manifest Manifest; + std::string Path; + }; + + // Fills in a manifest and gives it a unique path, along with the hash of that path. + void CreateFakeManifestAndPath( + ManifestAndPath& manifestAndPath, + const AppInstaller::Manifest::string_t& publisher, + std::string_view version = "1.0.0", + std::optional arpMinVersion = {}, + std::optional arpMaxVersion = {}); + + // The values that a test places into an index for a single manifest. + struct IndexFields + { + IndexFields( + std::string id, + std::string name, + std::string moniker, + std::string version, + std::string channel, + std::vector tags, + std::vector commands, + std::string path + ) : + Id(std::move(id)), + Name(std::move(name)), + Moniker(std::move(moniker)), + Version(std::move(version)), + Channel(std::move(channel)), + Tags(std::move(tags)), + Commands(std::move(commands)), + Path(std::move(path)) + {} + + IndexFields( + std::string id, + std::string name, + std::string moniker, + std::string version, + std::string channel, + std::vector tags, + std::vector commands, + std::string path, + std::vector packageFamilyNames, + std::vector productCodes + ) : + Id(std::move(id)), + Name(std::move(name)), + Moniker(std::move(moniker)), + Version(std::move(version)), + Channel(std::move(channel)), + Tags(std::move(tags)), + Commands(std::move(commands)), + Path(std::move(path)), + PackageFamilyNames(std::move(packageFamilyNames)), + ProductCodes(std::move(productCodes)) + {} + + IndexFields( + std::string id, + std::string name, + std::string publisher, + std::string moniker, + std::string version, + std::string channel, + std::vector tags, + std::vector commands, + std::string path, + std::vector packageFamilyNames, + std::vector productCodes + ) : + Id(std::move(id)), + Name(std::move(name)), + Publisher(std::move(publisher)), + Moniker(std::move(moniker)), + Version(std::move(version)), + Channel(std::move(channel)), + Tags(std::move(tags)), + Commands(std::move(commands)), + Path(std::move(path)), + PackageFamilyNames(std::move(packageFamilyNames)), + ProductCodes(std::move(productCodes)) + {} + + IndexFields( + std::string id, + std::string name, + std::string publisher, + std::string moniker, + std::string version, + std::string channel, + std::vector tags, + std::vector commands, + std::string path, + std::vector packageFamilyNames, + std::vector productCodes, + std::string arpName, + std::string arpPublisher + ) : + Id(std::move(id)), + Name(std::move(name)), + Publisher(std::move(publisher)), + Moniker(std::move(moniker)), + Version(std::move(version)), + Channel(std::move(channel)), + Tags(std::move(tags)), + Commands(std::move(commands)), + Path(std::move(path)), + PackageFamilyNames(std::move(packageFamilyNames)), + ProductCodes(std::move(productCodes)), + ArpName(std::move(arpName)), + ArpPublisher(std::move(arpPublisher)) + {} + + std::string Id; + std::string Name; + std::string Publisher; + std::string Moniker; + std::string Version; + std::string Channel; + std::vector Tags; + std::vector Commands; + std::string Path; + std::vector PackageFamilyNames; + std::vector ProductCodes; + std::string ArpName; + std::string ArpPublisher; + }; + + // Applies the given fields to a manifest, overwriting what it names and leaving the rest. + void ApplyIndexFields(AppInstaller::Manifest::Manifest& manifest, const IndexFields& fields); + + // Produces the manifest described by the given fields. + // + // Unlike ApplyIndexFields, nothing can carry over from a previously described package, which is + // what a test that replaces a package's data wants. + AppInstaller::Manifest::Manifest CreateManifest(const IndexFields& fields); + + // Creates an index containing the given data. + AppInstaller::Repository::Microsoft::SQLiteIndex SearchTestSetup( + const std::string& filePath, + std::initializer_list data = {}, + std::optional version = {}); +} From 86ff4542dd6ae0b1c312c42bf10dc44336b4d300 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 4 Sep 2026 14:08:40 -0700 Subject: [PATCH 29/36] Fixes and summary of failed tests --- src/AppInstallerCLITests/SQLiteIndexDelta.cpp | 84 ++++++++++++++++++- src/AppInstallerCLITests/main.cpp | 24 ++++++ .../Microsoft/Schema/2_0/Interface_2_0.cpp | 3 - .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 2 +- 4 files changed, 106 insertions(+), 7 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp index 6b00b935f2..9fe57fcb72 100644 --- a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp +++ b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp @@ -197,6 +197,15 @@ namespace index.AddManifest(manifest, fields.Path); } + // Adds to the working index without moving the change window, for setup that has to be + // part of the baseline rather than part of the delta. + void AddToWorking(const IndexFields& fields) + { + SQLiteIndex index = SQLiteIndex::Open(WorkingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + Manifest manifest = CreateManifest(fields); + index.AddManifest(manifest, fields.Path); + } + void Update(const IndexFields& fields) { SQLiteIndex index = OpenWorkingForChanges(); @@ -278,18 +287,21 @@ namespace std::vector tags = { "t1", "t2" }, std::vector commands = { "c1" }, std::vector packageFamilyNames = {}, - std::vector productCodes = {}) + std::vector productCodes = {}, + std::string version = "1.0"s) { + std::string path = id + "/" + version; + return IndexFields{ id, std::move(name), "Publisher"s, "moniker"s, - "1.0"s, + std::move(version), ""s, std::move(tags), std::move(commands), - id + "/1.0", + std::move(path), std::move(packageFamilyNames), std::move(productCodes) }; } @@ -584,6 +596,57 @@ TEST_CASE("SQLiteIndex_Delta_TrackingRowIdMatchesPreparedIndex", "[sqliteindex][ } } +// B12. Package identity is case insensitive everywhere in the index: the ids table collapses +// LIKE equal identifiers onto one rowid and overwrites the stored string with the most recent +// casing, while the tracking table freezes the casing it first saw. Generation therefore has to +// resolve a package across a casing difference between the two. +// +// On the changed path, byte equality fails to resolve the package and generation throws for the +// entire index. +TEST_CASE("SQLiteIndex_Delta_IdentifierCasingChange_Changed", "[sqliteindex][V2_1][delta]") +{ + DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1"), MakePackage("Publisher2.Id", "Package 2") } }; + + // Adding a version under a different casing rewrites the ids table entry, and with it the + // identifier that packaging will put in the packages table. The tracking row keeps the + // original casing. + context.Add(MakePackage("publisher1.id", "Package 1 V2", { "t1", "t2" }, { "c1" }, {}, {}, "2.0"s)); + + REQUIRE_NOTHROW(context.GenerateDelta()); + + SQLiteIndex combined = context.OpenCombined(); + + // Exactly one row for the package. Resolving to no rowid would have left the baseline row + // unsuppressed alongside the delta's, showing it twice. + REQUIRE(GetSearchedIds(combined) == std::set{ "publisher1.id", "Publisher2.Id" }); +} + +// B13. The silent half of the same defect. Here the casing changed before the baseline was taken, +// so the baseline holds the new casing while the tracking table still holds the old. A removal +// that cannot be resolved against the baseline is treated as "never existed there" and skipped, +// leaving the baseline row visible forever. +TEST_CASE("SQLiteIndex_Delta_IdentifierCasingChange_Removed", "[sqliteindex][V2_1][delta]") +{ + auto original = MakePackage("Publisher1.Id", "Package 1"); + auto recased = MakePackage("publisher1.id", "Package 1 V2", { "t1", "t2" }, { "c1" }, {}, {}, "2.0"s); + + DeltaTestContext context; + context.CreateWorking({ original, MakePackage("Publisher2.Id", "Package 2") }); + context.AddToWorking(recased); + context.CaptureBaseline(); + + REQUIRE(GetPreparedPackageRowId(context.BaselineFile.GetPath(), "publisher1.id").has_value()); + + context.Remove(original); + context.Remove(recased); + + context.GenerateDelta(); + + SQLiteIndex combined = context.OpenCombined(); + + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher2.Id" }); +} + // B11. The migration adds the rowid column to a table whose rows predate it. A backfill that left // nulls behind would break the first delta generated after an upgrade. TEST_CASE("SQLiteIndex_Delta_TrackingMigrationBackfillsRowIds", "[sqliteindex][V2_1][updatetracking]") @@ -1442,6 +1505,8 @@ namespace addFieldRequest(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, "Package Replacement", "Publisher"); addFieldRequest(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, "Package Original", "Publisher"); + size_t matchedRequests = 0; + for (size_t i = 0; i < requests.size(); ++i) { INFO("request index " << i); @@ -1465,8 +1530,18 @@ namespace REQUIRE(combinedIds.has_value() == fullIds.has_value()); REQUIRE(combinedIds == fullIds); + + if (fullIds && !fullIds->empty()) + { + ++matchedRequests; + } } + // Agreement is only meaningful if the battery actually matched something. Without this the + // group passes when every request throws or returns nothing, which is exactly what a defect + // in the merged views could cause. + REQUIRE(matchedRequests != 0); + // H3. The identifiers agreeing is not enough; the rowids that name them have to agree too, // since that is what the merge is built on and what a caller carries around. std::map combinedPrimaryIds; @@ -1483,6 +1558,9 @@ namespace REQUIRE(combinedPrimaryIds.size() == fullPrimaryIds.size()); + // H2 and H3 iterate this map, so an empty one would make both pass vacuously. + REQUIRE(!combinedPrimaryIds.empty()); + for (const auto& [packageId, primaryId] : combinedPrimaryIds) { INFO(packageId); diff --git a/src/AppInstallerCLITests/main.cpp b/src/AppInstallerCLITests/main.cpp index a91ca2ab11..77aaef8671 100644 --- a/src/AppInstallerCLITests/main.cpp +++ b/src/AppInstallerCLITests/main.cpp @@ -33,12 +33,22 @@ struct LoggingBreakListener : public Catch::EventListenerBase if (!testCaseStats.totals.delta(lastTotals).testCases.allOk()) { TestCommon::TempFile::SetTestFailed(true); + if (testCaseStats.testInfo) + { + GetFailedTests().push_back(testCaseStats.testInfo->name); + } } lastTotals = testCaseStats.totals; Catch::EventListenerBase::testCaseEnded(testCaseStats); } Catch::Totals lastTotals{}; + + static std::vector& GetFailedTests() + { + static std::vector failedTests; + return failedTests; + } }; CATCH_REGISTER_LISTENER(LoggingBreakListener); @@ -78,6 +88,7 @@ int main(int argc, char** argv) bool hasSetTestDataBasePath = false; bool waitBeforeReturn = false; bool keepSQLLogging = false; + bool printSummary = false; std::vector args; for (int i = 0; i < argc; ++i) @@ -132,6 +143,10 @@ int main(int argc, char** argv) Debugging::EnableSelfInitiatedMinidump(std::filesystem::path{ argv[i] }); } } + else if ("-summary"s == argv[i]) + { + printSummary = true; + } else { args.push_back(argv[i]); @@ -175,6 +190,15 @@ int main(int argc, char** argv) int result = Catch::Session().run(static_cast(args.size()), args.data()); + if (printSummary) + { + std::cout << "\nFailed tests:\n"; + for (const auto& test : LoggingBreakListener::GetFailedTests()) + { + std::cout << " " << test << "\n"; + } + } + if (waitBeforeReturn) { // Wait for some input before returning diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index d54f027c39..fdf0eaa2dd 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -649,9 +649,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 THROW_WIN32_IF(ERROR_INVALID_STATE, baseOutputDirectory.empty() || baseOutputDirectory.is_relative()); - // TEMP - PackageUpdateTrackingTable::EnsureExists(connection, m_trackingRemovalBehavior); - // Output all of the changed package version manifests since the base time to the target location for (const auto& packageData : PackageUpdateTrackingTable::GetUpdatesSince(connection, updateBaseTime, m_trackingRemovalBehavior)) { diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp index 640c53f68c..f44952a062 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -51,7 +51,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta { StatementBuilder builder; builder.Select(SQLite::RowIDName).From(V2_0::PackagesTable::TableName()). - Where(V2_0::PackagesTable::IdColumn::Name).Equals(packageIdentifier); + Where(V2_0::PackagesTable::IdColumn::Name).LikeWithEscape(packageIdentifier); SQLite::Statement statement = builder.Prepare(connection); From d18ecbc42ec0f9762f5a98e8d811e3e86e70e3aa Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 4 Sep 2026 14:17:57 -0700 Subject: [PATCH 30/36] set of case folded removes --- src/AppInstallerCLITests/SQLiteIndexDelta.cpp | 16 +++++++++------- .../Schema/2_0/PackageUpdateTrackingTable.cpp | 17 +++-------------- .../Schema/2_0/PackageUpdateTrackingTable.h | 8 +++++--- .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 2 +- .../Microsoft/Schema/2_1/DeltaGeneration.h | 3 ++- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp index 9fe57fcb72..281b95d944 100644 --- a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp +++ b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp @@ -458,14 +458,16 @@ TEST_CASE("SQLiteIndex_Delta_RemoveAddRemove", "[sqliteindex][V2_1][delta]") REQUIRE(GetScalar(working, "SELECT COUNT(*) FROM [update_tracking] WHERE [package] = 'Publisher2.Id' AND [is_removed] = 1") == 2); auto removals = Tracking::GetRemovalsSince(working, 0, Tracking::RemovalBehavior::Record); - REQUIRE(std::count(removals.begin(), removals.end(), "Publisher2.Id") == 1); + REQUIRE(removals == std::set{ "publisher2.id" }); } REQUIRE_NOTHROW(context.GenerateDelta()); { Connection delta = context.OpenDeltaConnection(); - REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'Publisher2.Id'") == 1); + // A removal carries the folded identifier, since that is the identity the tracking table + // reports and the casing of the individual tombstones need not agree. + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'publisher2.id'") == 1); REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [rowid] = " + std::to_string(originalRowId) + " AND [is_removed] = 1") == 1); } @@ -789,7 +791,7 @@ TEST_CASE("SQLiteIndex_Delta_RemovedPackage", "[sqliteindex][V2_1][delta]") Connection delta = context.OpenDeltaConnection(); REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [is_removed] = 1") == 1); - REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "Publisher2.Id" }); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "publisher2.id" }); // A removal carries no data beyond identity, so the rest of the row stays null. REQUIRE(GetScalar(delta, @@ -858,7 +860,7 @@ TEST_CASE("SQLiteIndex_Delta_MultipleChangesAndRemovals", "[sqliteindex][V2_1][d Connection delta = context.OpenDeltaConnection(); REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == - std::set{ "Publisher1.Id", "Publisher2.Id" }); + std::set{ "publisher1.id", "publisher2.id" }); REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 0") == std::set{ "Publisher3.Id", "Publisher5.Id", "Publisher6.Id" }); @@ -879,7 +881,7 @@ TEST_CASE("SQLiteIndex_Delta_PackageAddedAndRemovedWithinWindow", "[sqliteindex] REQUIRE_NOTHROW(context.GenerateDelta()); Connection delta = context.OpenDeltaConnection(); - REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'Transient.Id'") == 0); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] LIKE 'Transient.Id'") == 0); SQLiteIndex combined = context.OpenCombined(); REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id" }); @@ -901,7 +903,7 @@ TEST_CASE("SQLiteIndex_Delta_IdentifierWithLikeWildcards", "[sqliteindex][V2_1][ Connection delta = context.OpenDeltaConnection(); // Only the package actually removed; a LIKE would also have matched the decoy. - REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "Publisher_A.Id" }); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "publisher_a.id" }); SQLiteIndex combined = context.OpenCombined(); REQUIRE(GetSearchedIds(combined) == std::set{ "PublisherXA.Id", "Pub%cent.Id" }); @@ -1430,7 +1432,7 @@ TEST_CASE("SQLiteIndex_Delta_CheckConsistency_ReAddedPackageIsNotCorruption", "[ // The removal is still reported, since the old rowid genuinely was vacated. auto removals = Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record); - REQUIRE(std::count(removals.begin(), removals.end(), "Publisher2.Id") == 1); + REQUIRE(removals == std::set{ "publisher2.id" }); // And it is not also reported as an update under that identity being gone. auto updates = Tracking::GetUpdatesSince(connection, 0, Tracking::RemovalBehavior::Record); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp index f23abfbff1..a00c16c19c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp @@ -474,9 +474,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return result; } - std::vector PackageUpdateTrackingTable::GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals) + std::set PackageUpdateTrackingTable::GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals) { - std::vector result; + std::set result; if (removals == RemovalBehavior::Delete) { @@ -491,20 +491,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 Statement select = builder.Prepare(connection); - // A package that was removed, re-added at a different rowid, and removed again leaves a - // tombstone for each rowid it vacated. They all name the same package, and a consumer - // resolves that name against the baseline once, so report it once. Folding the case keeps - // this consistent with the ICU LIKE that decides package identity everywhere else. - std::set seen; - while (select.Step()) { - std::string packageIdentifier = select.GetColumn(0); - - if (seen.insert(Utility::FoldCase(static_cast(packageIdentifier))).second) - { - result.emplace_back(std::move(packageIdentifier)); - } + result.emplace(Utility::FoldCase(static_cast(select.GetColumn(0)))); } return result; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h index a5f9edaba3..749544ef39 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h @@ -4,6 +4,8 @@ #include "Microsoft/Schema/ISQLiteIndex.h" #include +#include + namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { @@ -64,10 +66,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Removed packages are never included; use GetRemovalsSince to retrieve those. static std::vector GetUpdatesSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); - // Gets the identifiers of the packages removed since the given base time. + // Gets the identifiers of the packages removed since the given base time, with their case + // folded so that a package appears once regardless of how its casing changed over time. // Only meaningful when removals are being recorded; always empty otherwise. - // A package that was removed more than once contributes a single entry. - static std::vector GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); + static std::set GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); // Gets the data hash for the given package identifier. static SQLite::blob_t GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier, RemovalBehavior removals); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp index f44952a062..acad155bf2 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -303,7 +303,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta const std::filesystem::path& deltaOutputPath, const SQLite::Version& version, const std::vector& changedPackages, - const std::vector& removedPackages) + const std::set& removedPackages) { AICLI_LOG(Repo, Info, << "Generating delta index at [" << deltaOutputPath << "] for " << changedPackages.size() << " changed and " << removedPackages.size() << " removed packages"); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h index 7b5ac960db..7420ba5301 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -29,5 +30,5 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta const std::filesystem::path& deltaOutputPath, const SQLite::Version& version, const std::vector& changedPackages, - const std::vector& removedPackages); + const std::set& removedPackages); } From a0ef4a366d8268c3d7874888a6803fa5982c110e Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 4 Sep 2026 15:51:31 -0700 Subject: [PATCH 31/36] Switch to package rowid for removals --- src/AppInstallerCLITests/SQLiteIndexDelta.cpp | 71 +++++++++++--- .../Microsoft/Schema/2_0/Interface_2_0.cpp | 13 ++- .../Schema/2_0/PackageUpdateTrackingTable.cpp | 98 +++++++++++-------- .../Schema/2_0/PackageUpdateTrackingTable.h | 12 ++- .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 67 ++++++++----- .../Microsoft/Schema/2_1/DeltaGeneration.h | 2 +- 6 files changed, 185 insertions(+), 78 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp index 281b95d944..f8365d9d94 100644 --- a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp +++ b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp @@ -435,8 +435,10 @@ TEST_CASE("SQLiteIndex_Delta_ReusedRowIdReplacesAssociations", "[sqliteindex][V2 REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id") == std::set{ "keep" }); } -// B6. Remove, re-add, and remove again leaves a tombstone for each rowid the package vacated. They -// all name the same package, and it resolves to one baseline rowid, so it must be reported once. +// B6. Remove, re-add, and remove again leaves a tombstone for each rowid the package vacated. Both +// are reported, but only one of them names a rowid the baseline holds, so only one removal is +// written — a delta that recorded the transient rowid too would claim to suppress a row that has +// never existed. TEST_CASE("SQLiteIndex_Delta_RemoveAddRemove", "[sqliteindex][V2_1][delta]") { auto p1 = MakePackage("Publisher1.Id", "Package 1"); @@ -454,20 +456,67 @@ TEST_CASE("SQLiteIndex_Delta_RemoveAddRemove", "[sqliteindex][V2_1][delta]") { Connection working = Connection::Create(context.WorkingFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); - // One tombstone per vacated rowid, but a single identifier reported to generation. + // One tombstone per vacated rowid, and generation is told about both of them. REQUIRE(GetScalar(working, "SELECT COUNT(*) FROM [update_tracking] WHERE [package] = 'Publisher2.Id' AND [is_removed] = 1") == 2); auto removals = Tracking::GetRemovalsSince(working, 0, Tracking::RemovalBehavior::Record); - REQUIRE(removals == std::set{ "publisher2.id" }); + REQUIRE(removals.size() == 2); + REQUIRE(removals.count(originalRowId) == 1); } REQUIRE_NOTHROW(context.GenerateDelta()); { Connection delta = context.OpenDeltaConnection(); - // A removal carries the folded identifier, since that is the identity the tracking table - // reports and the casing of the individual tombstones need not agree. - REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'publisher2.id'") == 1); + // Only the rowid the baseline actually holds is written; the one the re-add briefly + // occupied is above the baseline's range, so there is nothing there to suppress. + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'Publisher2.Id'") == 1); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [rowid] = " + std::to_string(originalRowId) + " AND [is_removed] = 1") == 1); + } + + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher3.Id" }); +} + +// B14. The same sequence, but the re-add changes the casing of the identifier. The tracking table +// freezes the casing of each row at insert, so the two tombstones carry byte different names. With +// identity settled on the rowid this is uneventful, which is exactly what it is asserting: no part +// of the removal path compares the two spellings, so nothing can get the comparison wrong. +TEST_CASE("SQLiteIndex_Delta_RemoveAddRemove_CasingChanged", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + auto p3 = MakePackage("Publisher3.Id", "Package 3"); + auto p2Recased = MakePackage("publisher2.id", "Package 2"); + + DeltaTestContext context{ { p1, p2, p3 } }; + + rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), "Publisher2.Id").value(); + + // Publisher3 holds the highest rowid, so the re-add lands on a new one and leaves the first + // tombstone in place rather than reviving it. + context.Remove(p2); + context.Add(p2Recased); + context.Remove(p2Recased); + + { + Connection working = Connection::Create(context.WorkingFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + + // Two tombstones at two rowids. The differing casing does not participate in identity at + // all, which is the point: nothing here has to reconcile the two spellings. + auto removals = Tracking::GetRemovalsSince(working, 0, Tracking::RemovalBehavior::Record); + REQUIRE(removals.size() == 2); + REQUIRE(removals.count(originalRowId) == 1); + } + + REQUIRE_NOTHROW(context.GenerateDelta()); + + { + Connection delta = context.OpenDeltaConnection(); + + // Only the baseline rowid was written, and the identifier stored with it is the one the + // baseline holds rather than either spelling the tracking table recorded. + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [is_removed] = 1") == 1); REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [rowid] = " + std::to_string(originalRowId) + " AND [is_removed] = 1") == 1); } @@ -791,7 +840,7 @@ TEST_CASE("SQLiteIndex_Delta_RemovedPackage", "[sqliteindex][V2_1][delta]") Connection delta = context.OpenDeltaConnection(); REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [is_removed] = 1") == 1); - REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "publisher2.id" }); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "Publisher2.Id" }); // A removal carries no data beyond identity, so the rest of the row stays null. REQUIRE(GetScalar(delta, @@ -860,7 +909,7 @@ TEST_CASE("SQLiteIndex_Delta_MultipleChangesAndRemovals", "[sqliteindex][V2_1][d Connection delta = context.OpenDeltaConnection(); REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == - std::set{ "publisher1.id", "publisher2.id" }); + std::set{ "Publisher1.Id", "Publisher2.Id" }); REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 0") == std::set{ "Publisher3.Id", "Publisher5.Id", "Publisher6.Id" }); @@ -903,7 +952,7 @@ TEST_CASE("SQLiteIndex_Delta_IdentifierWithLikeWildcards", "[sqliteindex][V2_1][ Connection delta = context.OpenDeltaConnection(); // Only the package actually removed; a LIKE would also have matched the decoy. - REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "publisher_a.id" }); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "Publisher_A.Id" }); SQLiteIndex combined = context.OpenCombined(); REQUIRE(GetSearchedIds(combined) == std::set{ "PublisherXA.Id", "Pub%cent.Id" }); @@ -1432,7 +1481,7 @@ TEST_CASE("SQLiteIndex_Delta_CheckConsistency_ReAddedPackageIsNotCorruption", "[ // The removal is still reported, since the old rowid genuinely was vacated. auto removals = Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record); - REQUIRE(removals == std::set{ "publisher2.id" }); + REQUIRE(removals.size() == 1); // And it is not also reported as an update under that identity being gone. auto updates = Tracking::GetUpdatesSince(connection, 0, Tracking::RemovalBehavior::Record); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index fdf0eaa2dd..17c70f68b7 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -140,10 +140,21 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { EnsureInternalInterface(connection, true); std::optional identifier = m_internalInterface->GetPropertyByPrimaryId(connection, manifestId, PackageVersionProperty::Id); + + // Resolve the rowid the package occupies while it is still present. If this removes its + // last version the ids row goes with it, and the value becomes unrecoverable; the tracking + // table needs it in order to record which rowid was vacated. + std::optional packageRowId; + + if (identifier && m_trackingRemovalBehavior == PackageUpdateTrackingTable::RemovalBehavior::Record) + { + packageRowId = V1_0::IdTable::SelectIdByValue(connection, identifier.value(), true); + } + m_internalInterface->RemoveManifestById(connection, manifestId); if (identifier) { - PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), identifier.value(), m_trackingRemovalBehavior); + PackageUpdateTrackingTable::Update(connection, m_internalInterface.get(), identifier.value(), m_trackingRemovalBehavior, true, packageRowId); } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp index a00c16c19c..6f28a4abbc 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp @@ -42,17 +42,32 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return V1_0::IdTable::SelectIdByValue(connection, packageIdentifier, true); } - // Determines whether the package currently has a row that is not marked as removed. - bool HasLiveRow(const SQLite::Connection& connection, const std::string& packageIdentifier) + // The tombstones, as the identifier recorded for the package and the rowid it vacated. + // Generation needs only the rowid, so the public accessor reports that; the consistency + // check needs both in order to say which package is at fault. + std::vector> GetRemovedRows( + const SQLite::Connection& connection, + PackageUpdateTrackingTable::RemovalBehavior removals) { + std::vector> result; + + if (removals == PackageUpdateTrackingTable::RemovalBehavior::Delete) + { + return result; + } + Builder::StatementBuilder builder; - builder.Select(Builder::RowCount).From(s_PUTT_Table_Name). - Where(s_PUTT_Package).LikeWithEscape(packageIdentifier). - And(s_PUTT_IsRemoved).Equals(0); + builder.Select({ s_PUTT_Package, s_PUTT_PackageRowId }).From(s_PUTT_Table_Name). + Where(s_PUTT_IsRemoved).Equals(1); Statement statement = builder.Prepare(connection); - THROW_HR_IF(E_UNEXPECTED, !statement.Step()); - return statement.GetColumn(0) != 0; + + while (statement.Step()) + { + result.emplace_back(statement.GetColumn(0), statement.GetColumn(1)); + } + + return result; } } @@ -79,9 +94,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 builder.Column(ColumnBuilder(s_PUTT_IsRemoved, Type::Int64).NotNull().Default(0)); // The rowid the package occupies in the index, which is what a delta is keyed on. - // 0 means unknown, which only arises for a removal of a package that was never - // tracked as present; generation resolves removals against the baseline anyway. - builder.Column(ColumnBuilder(s_PUTT_PackageRowId, Type::Int64).NotNull().Default(0)); + // Always known: the add path resolves it while the package is present, and the remove + // path is given it by the caller, which resolves it before the package leaves. + builder.Column(ColumnBuilder(s_PUTT_PackageRowId, Type::Int64).NotNull()); } builder.EndColumns(); @@ -143,7 +158,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return statement.GetColumn(0) != 0; } - void PackageUpdateTrackingTable::Update(SQLite::Connection& connection, const ISQLiteIndex* internalIndex, const std::string& packageIdentifier, RemovalBehavior removals, bool ensureTable) + void PackageUpdateTrackingTable::Update(SQLite::Connection& connection, const ISQLiteIndex* internalIndex, const std::string& packageIdentifier, RemovalBehavior removals, bool ensureTable, std::optional removedPackageRowId) { if (ensureTable) { @@ -167,10 +182,16 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 else { // Mark the package as removed rather than deleting the row, clearing the data columns. - // Only the live row is marked; earlier tombstones refer to rowids the package has - // already vacated and must be preserved so that a delta learns about each of them. - // The package rowid is carried forward untouched, since the package is already gone - // from the index and can no longer be looked up there. + // The row is found by the rowid the package occupied rather than by its identifier: + // that is the identity a delta is keyed on, and matching on it means a package whose + // identifier changed casing still marks the row it actually owns. Earlier tombstones + // refer to rowids the package has already vacated and must be preserved so that a + // delta learns about each of them. + // + // The rowid cannot be looked up here, because the package has already left the index. + // The caller resolves it beforehand and passes it in. + THROW_HR_IF(E_NOT_VALID_STATE, !removedPackageRowId); + int64_t currentTime = Utility::GetCurrentUnixEpoch(); Builder::StatementBuilder updateBuilder; @@ -179,7 +200,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 Column(s_PUTT_Manifest).AssignValue(nullptr). Column(s_PUTT_Hash).AssignValue(nullptr). Column(s_PUTT_IsRemoved).Equals(1). - Where(s_PUTT_Package).LikeWithEscape(packageIdentifier). + Where(s_PUTT_PackageRowId).Equals(removedPackageRowId.value()). And(s_PUTT_IsRemoved).Equals(0); updateBuilder.Execute(connection); @@ -187,13 +208,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { // The package was added and removed without an intervening tracking checkpoint, // so there is no row to mark. Record the removal so that a delta built against an - // older baseline still learns that the package is gone. The rowid is unknown - // because the package is no longer in the index; generation does not need it, - // as it resolves a removal against the baseline by identifier. + // older baseline still learns that the rowid was vacated. Builder::StatementBuilder insertBuilder; insertBuilder.InsertInto(s_PUTT_Table_Name). - Columns({ s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_IsRemoved }). - Values(packageIdentifier, currentTime, 1); + Columns({ s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_IsRemoved, s_PUTT_PackageRowId }). + Values(packageIdentifier, currentTime, 1, removedPackageRowId.value()); insertBuilder.Execute(connection); } } @@ -338,21 +357,15 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } - // Any package recorded as removed must no longer be in the internal index - for (const std::string& packageIdentifier : GetRemovalsSince(connection, 0, removals)) + // A package recorded as removed must no longer occupy the rowid it vacated. Comparing the + // rowid rather than merely checking for absence is what makes this precise: a package that + // was removed and re-added is legitimately back in the index, but at a different rowid, and + // the tombstone for the one it gave up is still meaningful. + for (const auto& [packageIdentifier, vacatedRowId] : GetRemovedRows(connection, removals)) { - // A package that was removed and later re-added keeps the tombstone for the rowid it - // vacated alongside a live row for the rowid it now occupies. Its presence in the - // index is therefore expected, and only the live row describes it. - if (HasLiveRow(connection, packageIdentifier)) - { - continue; - } - - SearchRequest request; - request.Inclusions.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, packageIdentifier); + std::optional indexRowId = GetPackageRowIdInIndex(connection, packageIdentifier); - if (!internalIndex->Search(connection, request).Matches.empty()) + if (indexRowId && indexRowId.value() == vacatedRowId) { if (!log) { @@ -361,7 +374,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 result = false; AICLI_LOG(Repo, Info, << " [INVALID] value [" << s_PUTT_Package << "] in table [" << s_PUTT_Table_Name << - "]; the package [" << packageIdentifier << "] is marked as removed but is present in the internal index"); + "]; the package [" << packageIdentifier << "] is marked as having vacated rowid [" << vacatedRowId << + "] but still occupies it in the internal index"); } } @@ -474,9 +488,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return result; } - std::set PackageUpdateTrackingTable::GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals) + std::set PackageUpdateTrackingTable::GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals) { - std::set result; + std::set result; if (removals == RemovalBehavior::Delete) { @@ -485,15 +499,21 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } Builder::StatementBuilder builder; - builder.Select(s_PUTT_Package).From(s_PUTT_Table_Name). + builder.Select(s_PUTT_PackageRowId).From(s_PUTT_Table_Name). Where(s_PUTT_WriteTime).IsGreaterThanOrEqualTo(updateBaseTime). And(s_PUTT_IsRemoved).Equals(1); Statement select = builder.Prepare(connection); + // The rowid is reported rather than the identifier because it is the identity a delta is + // keyed on, and it is the only one that can be compared exactly. Identifiers cannot: the + // casing recorded for a package is frozen when its row is written, so one package can leave + // tombstones under several spellings, and no string comparison available here reproduces + // the ICU LIKE that decides identity elsewhere. Two packages can still vacate the same + // rowid in turn, so the result is a set. while (select.Step()) { - result.emplace(Utility::FoldCase(static_cast(select.GetColumn(0)))); + result.emplace(select.GetColumn(0)); } return result; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h index 749544ef39..4051180df8 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h @@ -43,7 +43,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 static bool Exists(const SQLite::Connection& connection); // Updates the tracking table for the given package identifier in the internal index. - static void Update(SQLite::Connection& connection, const ISQLiteIndex* internalIndex, const std::string& packageIdentifier, RemovalBehavior removals, bool ensureTable = true); + // When the package is no longer present and removals are being recorded, removedPackageRowId + // must carry the rowid it occupied; the caller has to resolve that before removing it from + // the index, as the ids row is gone by the time this is called. + static void Update(SQLite::Connection& connection, const ISQLiteIndex* internalIndex, const std::string& packageIdentifier, RemovalBehavior removals, bool ensureTable = true, std::optional removedPackageRowId = {}); // Checks the consistency of the index to ensure that every referenced row exists. // Returns true if index is consistent; false if it is not. @@ -66,10 +69,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Removed packages are never included; use GetRemovalsSince to retrieve those. static std::vector GetUpdatesSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); - // Gets the identifiers of the packages removed since the given base time, with their case - // folded so that a package appears once regardless of how its casing changed over time. + // Gets the rowids vacated by packages removed since the given base time. // Only meaningful when removals are being recorded; always empty otherwise. - static std::set GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); + // The rowid is reported rather than the identifier because it is the identity a delta is + // keyed on and the only one that can be compared exactly; see the implementation. + static std::set GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); // Gets the data hash for the given package identifier. static SQLite::blob_t GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier, RemovalBehavior removals); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp index acad155bf2..394a134d67 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -63,6 +63,23 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta return {}; } + // Gets the identifier of the package at the given rowid, if there is one. + std::optional SelectPackageIdByRowId(const SQLite::Connection& connection, SQLite::rowid_t packageRowId) + { + StatementBuilder builder; + builder.Select(V2_0::PackagesTable::IdColumn::Name).From(V2_0::PackagesTable::TableName()). + Where(SQLite::RowIDName).Equals(packageRowId); + + SQLite::Statement statement = builder.Prepare(connection); + + if (statement.Step()) + { + return statement.GetColumn(0); + } + + return {}; + } + // Gets the rowid of the given value in a one to many data table, if it is present. std::optional SelectValueRowId( const SQLite::Connection& connection, @@ -303,7 +320,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta const std::filesystem::path& deltaOutputPath, const SQLite::Version& version, const std::vector& changedPackages, - const std::set& removedPackages) + const std::set& removedPackages) { AICLI_LOG(Repo, Info, << "Generating delta index at [" << deltaOutputPath << "] for " << changedPackages.size() << " changed and " << removedPackages.size() << " removed packages"); @@ -328,9 +345,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta nextValueRowIds[table.TableName] = GetMaximumRowId(baselineConnection, table.TableName); } - // The changed packages are written first so that the removals can tell whether the rowid - // they are about to vacate has already been taken by one of them. - std::set claimedRowIds; + // Every rowid already written to the delta's packages table. The changed packages are + // written first so that a removal can tell whether the rowid it is about to vacate has + // since been taken, and each removal joins the set so that two tombstones resolving to one + // baseline rowid cannot both be written. + std::set writtenRowIds; for (const auto& package : changedPackages) { @@ -341,8 +360,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta AICLI_LOG(Repo, Verbose, << "Delta: recording change to [" << package.PackageIdentifier << "] (rowid " << packageRowId.value() << ")"); - claimedRowIds.insert(packageRowId.value()); - + writtenRowIds.insert(packageRowId.value()); WriteChangedPackage(deltaConnection, sourceConnection, packageRowId.value()); for (const auto& table : SystemReferenceTables()) @@ -356,33 +374,38 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta } } - for (const std::string& packageIdentifier : removedPackages) + for (SQLite::rowid_t removedRowId : removedPackages) { - std::optional packageRowId = SelectPackageRowId(baselineConnection, packageIdentifier); + // The rowid is resolved against the baseline directly, which is exact and is a primary + // key lookup. Whatever identifier the tracking table recorded is irrelevant here: what + // the delta suppresses is the baseline row at this rowid, so that row is also where the + // identifier stored alongside the tombstone comes from. + std::optional baselinePackageId = SelectPackageIdByRowId(baselineConnection, removedRowId); - if (!packageRowId) + if (!baselinePackageId) { - // The package was both added and removed since the baseline was produced, so as far - // as the baseline is concerned it never existed and there is nothing to suppress. - AICLI_LOG(Repo, Verbose, << "Delta: [" << packageIdentifier << "] was removed but is not in the baseline"); + // The rowid was allocated after the baseline was produced, so as far as the + // baseline is concerned it never held anything and there is nothing to suppress. + AICLI_LOG(Repo, Verbose, << "Delta: rowid " << removedRowId << " was vacated but is not in the baseline"); continue; } - if (claimedRowIds.count(packageRowId.value())) + if (writtenRowIds.count(removedRowId)) { - // Another package has taken the rowid this one vacated. Recording the removal is - // both impossible, since the rowid is the primary key of the delta's package table, - // and unnecessary: the row written for the new occupant already suppresses the - // baseline row, and the association differences are computed against the baseline - // at that same rowid, so the old package's data is displaced entirely. - AICLI_LOG(Repo, Verbose, << "Delta: [" << packageIdentifier << "] was removed but its rowid " << - packageRowId.value() << " is now held by a changed package"); + // The rowid has already been written, either by a package that has since taken it + // or by an earlier tombstone that vacated it. Writing it again is both impossible, + // since the rowid is the primary key of the delta's package table, and unnecessary: + // the row already there suppresses the baseline row, and where a new occupant wrote + // it the association differences were computed against the baseline at that same + // rowid, so the old package's data is displaced entirely. + AICLI_LOG(Repo, Verbose, << "Delta: rowid " << removedRowId << " was vacated but has already been written"); continue; } - AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << packageIdentifier << "] (rowid " << packageRowId.value() << ")"); + AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << baselinePackageId.value() << "] (rowid " << removedRowId << ")"); - WriteRemovedPackage(deltaConnection, packageRowId.value(), packageIdentifier); + writtenRowIds.insert(removedRowId); + WriteRemovedPackage(deltaConnection, removedRowId, baselinePackageId.value()); } savepoint.Commit(); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h index 7420ba5301..d6fcda5b7a 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.h @@ -30,5 +30,5 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta const std::filesystem::path& deltaOutputPath, const SQLite::Version& version, const std::vector& changedPackages, - const std::set& removedPackages); + const std::set& removedPackages); } From 2a8b5621ba9fc12b0926605addee0becff7ded9a Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 4 Sep 2026 16:16:37 -0700 Subject: [PATCH 32/36] test fixes --- src/AppInstallerCLITests/SQLiteIndexDelta.cpp | 12 +++++++++-- .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 1 - .../Microsoft/Schema/2_1/DeltaTables.cpp | 21 ++++++------------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp index f8365d9d94..f7e7aef22c 100644 --- a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp +++ b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp @@ -157,8 +157,15 @@ namespace // Copies the working index out, prepares it, and designates it as a baseline. // Everything the working index does afterwards is what the delta will describe. + // + // The sleep is load bearing. Preparing the baseline records the time from which a delta + // against it is computed, and tracking times are whole seconds compared inclusively, so a + // baseline captured in the same second as the data it holds would report all of it as + // changed. Advancing past that second is what makes the boundary observable. void CaptureBaseline(bool markAsBaseline = true) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + std::filesystem::copy_file(WorkingFile.GetPath(), BaselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); SQLiteIndex prepared = SQLiteIndex::Open(BaselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); @@ -174,8 +181,9 @@ namespace // Opens the working index for the changes that the delta will carry. // - // The base time is reset to now so that only what follows is considered changed, and the - // sleep is what makes that boundary observable: the tracking table stores whole seconds. + // The base time reset governs the version data manifest export that preparing performs; the + // window the delta itself uses comes from the baseline, and was fixed when it was captured. + // The sleep is what makes this boundary observable, as tracking times are whole seconds. SQLiteIndex OpenWorkingForChanges() { SQLiteIndex index = SQLiteIndex::Open(WorkingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp index 394a134d67..754d9633c3 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -404,7 +404,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta AICLI_LOG(Repo, Verbose, << "Delta: recording removal of [" << baselinePackageId.value() << "] (rowid " << removedRowId << ")"); - writtenRowIds.insert(removedRowId); WriteRemovedPackage(deltaConnection, removedRowId, baselinePackageId.value()); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp index b6cacc3c19..ffaa5a2b63 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp @@ -97,13 +97,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta }); builder.Execute(connection); - // Generation must never write two rows for the same package, and the delta is small - // enough that the index costs little while it is being built. It is dropped again - // before the delta ships; see PrepareTablesForPackaging. - StatementBuilder indexBuilder; - indexBuilder.CreateUniqueIndex({ tableName, s_Delta_ValueIndexSuffix }). - On(tableName).Columns(V2_0::PackagesTable::IdColumn::Name); - indexBuilder.Execute(connection); + // No index on the identifier. Identity here is the rowid, and one identifier can + // legitimately occupy two rows: a package removed and re-added within the window + // vacates one rowid and takes another, which is recorded as a removal at the first and + // a change at the second. Uniqueness on the rowid is already given by the primary key. } // The system reference tables hold the value itself, so the delta only adds the removal flag. @@ -164,9 +161,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta { using namespace SQLite::Builder; - // Every index here exists only to serve generation: the one on the packages table enforces - // that a package is recorded once, and those on the one to many data tables let generation - // find the rowid it already allocated for a value. Nothing reads them. + // Every index here exists only to serve generation: those on the one to many data tables + // let generation find the rowid it already allocated for a value. Nothing reads them. // // The merged views need no index at all. They suppress baseline packages by rowid, and // baseline associations by the (value, package) pair that is the primary key of a WITHOUT @@ -175,11 +171,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta { SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "delta_preparetables_v2_1"); - StatementBuilder packagesBuilder; - auto packagesTableName = GetTableName(V2_0::PackagesTable::TableName()); - packagesBuilder.DropIndex({ packagesTableName, s_Delta_ValueIndexSuffix }); - packagesBuilder.Execute(connection); - for (const auto& table : OneToManyTables()) { StatementBuilder builder; From 3b45e06cc1e4008fc0fb695e3a59f2f233c9265c Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Thu, 10 Sep 2026 16:31:17 -0700 Subject: [PATCH 33/36] Move to change sequence for delta --- src/AppInstallerCLITests/SQLiteIndexDelta.cpp | 165 +++++++++++- .../Schema/2_0/PackageUpdateTrackingTable.cpp | 255 +++++++++++++----- .../Schema/2_0/PackageUpdateTrackingTable.h | 15 ++ .../Microsoft/Schema/2_1/Interface.h | 5 + .../Microsoft/Schema/2_1/Interface_2_1.cpp | 38 ++- 5 files changed, 396 insertions(+), 82 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp index f7e7aef22c..489874e132 100644 --- a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp +++ b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp @@ -17,13 +17,11 @@ #include #include -#include #include #include #include #include #include -#include #include #include @@ -157,15 +155,8 @@ namespace // Copies the working index out, prepares it, and designates it as a baseline. // Everything the working index does afterwards is what the delta will describe. - // - // The sleep is load bearing. Preparing the baseline records the time from which a delta - // against it is computed, and tracking times are whole seconds compared inclusively, so a - // baseline captured in the same second as the data it holds would report all of it as - // changed. Advancing past that second is what makes the boundary observable. void CaptureBaseline(bool markAsBaseline = true) { - std::this_thread::sleep_for(std::chrono::seconds(1)); - std::filesystem::copy_file(WorkingFile.GetPath(), BaselineFile.GetPath(), std::filesystem::copy_options::overwrite_existing); SQLiteIndex prepared = SQLiteIndex::Open(BaselineFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); @@ -182,8 +173,8 @@ namespace // Opens the working index for the changes that the delta will carry. // // The base time reset governs the version data manifest export that preparing performs; the - // window the delta itself uses comes from the baseline, and was fixed when it was captured. - // The sleep is what makes this boundary observable, as tracking times are whole seconds. + // window the delta itself uses comes from the baseline's change sequence, and was fixed when + // the baseline was captured. SQLiteIndex OpenWorkingForChanges() { SQLiteIndex index = SQLiteIndex::Open(WorkingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); @@ -191,7 +182,6 @@ namespace if (!m_baseTimeReset) { index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); - std::this_thread::sleep_for(std::chrono::seconds(1)); m_baseTimeReset = true; } @@ -1707,3 +1697,154 @@ TEST_CASE("SQLiteIndex_Delta_EquivalenceWithEmptyDelta", "[sqliteindex][V2_1][de RequireEquivalent(combined, full); } + + +// --------------------------------------------------------------------------------------------- +// Group K - the change sequence +// +// The window a delta describes is defined by a monotonic sequence rather than by the write time. +// These cases cover the boundary that a whole second time cannot express, and the one failure +// mode a sequence has that a time does not. +// --------------------------------------------------------------------------------------------- + +// K1. Every write takes a new sequence, and a package written twice keeps only the later one. +// A sequence that did not advance on update would leave the second change outside any window +// opened after the first. +TEST_CASE("SQLiteIndex_Delta_ChangeSequenceAdvancesOnEveryWrite", "[sqliteindex][V2_1][updatetracking]") +{ + TempFile indexFile{ "changeseq"s, ".db"s }; + + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, s_DeltaVersion); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + + Manifest m1 = CreateManifest(p1); + index.AddManifest(m1, p1.Path); + + Manifest m2 = CreateManifest(p2); + index.AddManifest(m2, p2.Path); + + Manifest m1Updated = CreateManifest(MakePackage("Publisher1.Id", "Package 1 Renamed")); + REQUIRE(index.UpdateManifest(m1Updated, p1.Path)); + } + + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadOnly); + + // Three writes, but only two rows: the update replaced the first package's sequence rather + // than adding a row. + REQUIRE(GetRowCount(connection, "update_tracking") == 2); + REQUIRE(GetScalar(connection, "SELECT MAX([change_seq]) FROM [update_tracking]") == 3); + + int64_t first = GetScalar(connection, "SELECT [change_seq] FROM [update_tracking] WHERE [package] = 'Publisher1.Id'"); + int64_t second = GetScalar(connection, "SELECT [change_seq] FROM [update_tracking] WHERE [package] = 'Publisher2.Id'"); + + // The updated package is now the more recent of the two, having started as the older. + REQUIRE(first == 3); + REQUIRE(second == 2); +} + +// K2. The boundary case that the time based window could not express at all: a baseline captured +// in the same instant as the data it holds. Under whole second times compared inclusively, the +// entire baseline fell inside its own delta window. +TEST_CASE("SQLiteIndex_Delta_BaselineCapturedImmediatelyExcludesItsOwnData", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + + // No delay anywhere: creation, capture and generation all happen as fast as they can. + DeltaTestContext context{ { p1, p2 } }; + context.GenerateDelta(); + + { + Connection baseline = Connection::Create(context.BaselineFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + + // Both packages were written before the baseline was taken, so the baseline's own sequence + // must already account for them. + REQUIRE(MetadataTable::GetNamedValue(baseline, "deltaBaselineSequence") == 2); + } + + Connection delta = context.OpenDeltaConnection(); + REQUIRE(GetRowCount(delta, "delta_packages") == 0); + + SQLiteIndex combined = context.OpenCombined(); + REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher2.Id" }); +} + +// K3. A migrated table has no sequences, so every row backfills to the same value and the first +// one issued afterwards is above it. An index designated as a baseline at that moment records 0, +// and the exclusive window correctly reports nothing that preceded the migration. +TEST_CASE("SQLiteIndex_Delta_TrackingMigrationBackfillsChangeSequence", "[sqliteindex][V2_1][updatetracking]") +{ + TempFile indexFile{ "changeseq_migrate"s, ".db"s }; + + ManifestAndPath m1; + CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); + ManifestAndPath m2; + CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + + { + SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 0 }); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + index.AddManifest(m1.Manifest, m1.Path); + index.AddManifest(m2.Manifest, m2.Path); + } + + { + SQLiteIndex index = SQLiteIndex::Open(indexFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + REQUIRE(index.MigrateTo(s_DeltaVersion)); + REQUIRE(index.CheckConsistency(true)); + } + + { + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadOnly); + REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [change_seq] = 0") == 2); + } + + // A write after the migration has to be distinguishable from everything that preceded it. + { + SQLiteIndex index = SQLiteIndex::Open(indexFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + ManifestAndPath m3; + CreateFakeManifestAndPath(m3, "Publisher3", "1.0"); + index.AddManifest(m3.Manifest, m3.Path); + } + + Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadOnly); + REQUIRE(GetScalar(connection, "SELECT [change_seq] FROM [update_tracking] WHERE [package] = 'Publisher3.Id'") == 1); +} + +// K4. The one direction a sequence fails in that a time does not. If the working index is rebuilt, +// its counter restarts below the baseline's and the window is empty, which would produce a +// silently empty delta. Generation has to refuse instead. +TEST_CASE("SQLiteIndex_Delta_SequenceBelowBaselineIsRejected", "[sqliteindex][V2_1][delta]") +{ + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + auto p3 = MakePackage("Publisher3.Id", "Package 3"); + + DeltaTestContext context{ { p1, p2, p3 } }; + + { + Connection baseline = Connection::Create(context.BaselineFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); + REQUIRE(MetadataTable::GetNamedValue(baseline, "deltaBaselineSequence") == 3); + } + + // A rebuilt working index, holding the same data but having recorded far fewer changes. + TempFile rebuiltFile{ "changeseq_rebuilt"s, ".db"s }; + TempFile rebuiltDeltaFile{ "changeseq_rebuilt_delta"s, ".db"s }; + + { + SQLiteIndex index = SQLiteIndex::CreateNew(rebuiltFile, s_DeltaVersion); + index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, "0"); + Manifest manifest = CreateManifest(p1); + index.AddManifest(manifest, p1.Path); + } + + SQLiteIndex rebuilt = SQLiteIndex::Open(rebuiltFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + rebuilt.SetProperty(SQLiteIndex::Property::DeltaBaselineIndexPath, context.BaselineFile.GetPath().u8string()); + rebuilt.SetProperty(SQLiteIndex::Property::DeltaOutputPath, rebuiltDeltaFile.GetPath().u8string()); + + REQUIRE_THROWS_HR(rebuilt.PrepareForPackaging(), APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp index 6f28a4abbc..8e630e1227 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp @@ -27,6 +27,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 static constexpr std::string_view s_PUTT_Hash = "hash"sv; static constexpr std::string_view s_PUTT_IsRemoved = "is_removed"sv; static constexpr std::string_view s_PUTT_PackageRowId = "package_rowid"sv; + static constexpr std::string_view s_PUTT_ChangeSequence = "change_seq"sv; + static constexpr std::string_view s_PUTT_ChangeSequenceIndex_Name = "update_tracking_change_seq_idx"sv; namespace { @@ -69,6 +71,138 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 return result; } + + // The sequence to stamp on the row about to be written. + // + // Allocated as one above the highest ever issued rather than from a stored counter, which + // needs no separate value to keep in step with the table and no migration when it is + // absent. It never goes backwards: rows are only ever inserted or updated in place while + // removals are being recorded, so no sequence is released once issued. SQLite answers the + // aggregate from the index with a single seek. + int64_t GetNextChangeSequence(const SQLite::Connection& connection) + { + Builder::StatementBuilder builder; + builder.Select().Column(Builder::Aggregate::Max, s_PUTT_ChangeSequence).From(s_PUTT_Table_Name); + + Statement statement = builder.Prepare(connection); + + // The aggregate produces a single row holding null when the table is empty. + if (statement.Step() && !statement.GetColumnIsNull(0)) + { + return statement.GetColumn(0) + 1; + } + + return 1; + } + + // The rows written after the given point, as measured by the given column. + // + // The boundary is exclusive for the sequence and inclusive for the write time. That is not + // an inconsistency: whole second times cannot distinguish "written during the base second, + // before the base was taken" from "after", so the inclusive form is the only safe one and + // it over-reports by design. A sequence has no such ambiguity, so it takes the exact + // boundary and reports precisely what followed. + std::vector GetUpdates( + const SQLite::Connection& connection, + std::string_view boundaryColumn, + int64_t boundaryValue, + bool exclusive, + PackageUpdateTrackingTable::RemovalBehavior removals) + { + bool recordingRemovals = (removals == PackageUpdateTrackingTable::RemovalBehavior::Record); + + Builder::StatementBuilder builder; + + if (recordingRemovals) + { + builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash, s_PUTT_PackageRowId }); + } + else + { + builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash }); + } + + builder.From(s_PUTT_Table_Name).Where(boundaryColumn); + + if (exclusive) + { + builder.IsGreaterThan(boundaryValue); + } + else + { + builder.IsGreaterThanOrEqualTo(boundaryValue); + } + + if (recordingRemovals) + { + // Removals are reported separately, so that this remains the set of packages that + // have data to write out, exactly as it is when removals delete their row. + builder.And(s_PUTT_IsRemoved).Equals(0); + } + + Statement select = builder.Prepare(connection); + + std::vector result; + + while (select.Step()) + { + PackageUpdateTrackingTable::PackageData item; + item.RowID = select.GetColumn(0); + item.PackageIdentifier = select.GetColumn(1); + item.WriteTime = select.GetColumn(2); + item.Manifest = select.GetColumn(3); + item.Hash = select.GetColumn(4); + + if (recordingRemovals) + { + item.PackageRowId = select.GetColumn(5); + } + + result.emplace_back(std::move(item)); + } + + return result; + } + + // The rowids vacated after the given point, as measured by the given column. + // + // The rowid is reported rather than the identifier because it is the identity a delta is + // keyed on, and it is the only one that can be compared exactly. Identifiers cannot: the + // casing recorded for a package is frozen when its row is written, so one package can leave + // tombstones under several spellings, and no string comparison available here reproduces + // the ICU LIKE that decides identity elsewhere. Two packages can still vacate the same + // rowid in turn, so the result is a set. + std::set GetRemovals( + const SQLite::Connection& connection, + std::string_view boundaryColumn, + int64_t boundaryValue, + bool exclusive) + { + Builder::StatementBuilder builder; + builder.Select(s_PUTT_PackageRowId).From(s_PUTT_Table_Name).Where(boundaryColumn); + + if (exclusive) + { + builder.IsGreaterThan(boundaryValue); + } + else + { + builder.IsGreaterThanOrEqualTo(boundaryValue); + } + + builder.And(s_PUTT_IsRemoved).Equals(1); + + Statement select = builder.Prepare(connection); + + std::set result; + + while (select.Step()) + { + result.emplace(select.GetColumn(0)); + } + + return result; + } } std::string_view PackageUpdateTrackingTable::TableName() @@ -97,6 +231,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Always known: the add path resolves it while the package is present, and the remove // path is given it by the caller, which resolves it before the package leaves. builder.Column(ColumnBuilder(s_PUTT_PackageRowId, Type::Int64).NotNull()); + + // A monotonically increasing stamp identifying when this row was last written, + // relative only to the other rows in this table. See GetUpdatesSinceSequence for why + // a delta uses this in preference to the write time. + builder.Column(ColumnBuilder(s_PUTT_ChangeSequence, Type::Int64).NotNull()); } builder.EndColumns(); @@ -110,9 +249,21 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (removals == RemovalBehavior::Record) { CreateLiveRowIndex(connection); + CreateChangeSequenceIndex(connection); } } + void PackageUpdateTrackingTable::CreateChangeSequenceIndex(SQLite::Connection& connection) + { + // Serves both the range scan that reports changes and the maximum that allocates the next + // sequence. Not unique: a row updated in place takes a new sequence and leaves none behind, + // but nothing depends on two rows never sharing one, and the migration backfills every + // existing row with the same value. + Builder::StatementBuilder builder; + builder.CreateIndex(s_PUTT_ChangeSequenceIndex_Name).On(s_PUTT_Table_Name).Columns(s_PUTT_ChangeSequence); + builder.Execute(connection); + } + void PackageUpdateTrackingTable::CreateLiveRowIndex(SQLite::Connection& connection) { // A package occupies exactly one rowid at a time, so at most one row per rowid can be @@ -193,10 +344,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 THROW_HR_IF(E_NOT_VALID_STATE, !removedPackageRowId); int64_t currentTime = Utility::GetCurrentUnixEpoch(); + int64_t changeSequence = GetNextChangeSequence(connection); Builder::StatementBuilder updateBuilder; updateBuilder.Update(s_PUTT_Table_Name).Set(). Column(s_PUTT_WriteTime).Equals(currentTime). + Column(s_PUTT_ChangeSequence).Equals(changeSequence). Column(s_PUTT_Manifest).AssignValue(nullptr). Column(s_PUTT_Hash).AssignValue(nullptr). Column(s_PUTT_IsRemoved).Equals(1). @@ -211,8 +364,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // older baseline still learns that the rowid was vacated. Builder::StatementBuilder insertBuilder; insertBuilder.InsertInto(s_PUTT_Table_Name). - Columns({ s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_IsRemoved, s_PUTT_PackageRowId }). - Values(packageIdentifier, currentTime, 1, removedPackageRowId.value()); + Columns({ s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_IsRemoved, s_PUTT_PackageRowId, s_PUTT_ChangeSequence }). + Values(packageIdentifier, currentTime, 1, removedPackageRowId.value(), changeSequence); insertBuilder.Execute(connection); } } @@ -251,12 +404,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // recorded alongside the data. It is resolved here rather than at removal time because // the package is still in the index at this point. SQLite::rowid_t packageRowId = 0; + int64_t changeSequence = 0; if (removals == RemovalBehavior::Record) { std::optional indexRowId = GetPackageRowIdInIndex(connection, packageIdentifier); THROW_HR_IF(E_NOT_VALID_STATE, !indexRowId); packageRowId = indexRowId.value(); + changeSequence = GetNextChangeSequence(connection); } // First attempt to update the row and then insert it if no modification occurred. @@ -270,6 +425,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { // Clear the flag in case this package was previously removed and is now being re-added. updateBuilder.Column(s_PUTT_IsRemoved).Equals(0); + updateBuilder.Column(s_PUTT_ChangeSequence).Equals(changeSequence); } updateBuilder.Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); @@ -300,6 +456,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (removals == RemovalBehavior::Record) { insertBuilder.Column(s_PUTT_PackageRowId); + insertBuilder.Column(s_PUTT_ChangeSequence); } insertBuilder.EndColumns().BeginValues(); @@ -312,6 +469,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (removals == RemovalBehavior::Record) { insertBuilder.Value(packageRowId); + insertBuilder.Value(changeSequence); } insertBuilder.EndValues(); @@ -442,81 +600,47 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 std::vector PackageUpdateTrackingTable::GetUpdatesSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals) { - bool recordingRemovals = (removals == RemovalBehavior::Record); - - Builder::StatementBuilder builder; - - if (recordingRemovals) - { - builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash, s_PUTT_PackageRowId }); - } - else - { - builder.Select({ RowIDName, s_PUTT_Package, s_PUTT_WriteTime, s_PUTT_Manifest, s_PUTT_Hash }); - } - - builder.From(s_PUTT_Table_Name).Where(s_PUTT_WriteTime).IsGreaterThanOrEqualTo(updateBaseTime); - - if (recordingRemovals) - { - // Removals are reported separately, so that this remains the set of packages that - // have data to write out, exactly as it is when removals delete their row. - builder.And(s_PUTT_IsRemoved).Equals(0); - } - - Statement select = builder.Prepare(connection); - - std::vector result; - - while (select.Step()) - { - PackageData item; - item.RowID = select.GetColumn(0); - item.PackageIdentifier = select.GetColumn(1); - item.WriteTime = select.GetColumn(2); - item.Manifest = select.GetColumn(3); - item.Hash = select.GetColumn(4); - - if (recordingRemovals) - { - item.PackageRowId = select.GetColumn(5); - } + return GetUpdates(connection, s_PUTT_WriteTime, updateBaseTime, false, removals); + } - result.emplace_back(std::move(item)); - } + std::vector PackageUpdateTrackingTable::GetUpdatesSinceSequence(const SQLite::Connection& connection, int64_t baseSequence, RemovalBehavior removals) + { + // Only the recording form has the column, and only a delta asks this question. + THROW_HR_IF(E_NOT_VALID_STATE, removals != RemovalBehavior::Record); - return result; + return GetUpdates(connection, s_PUTT_ChangeSequence, baseSequence, true, removals); } std::set PackageUpdateTrackingTable::GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals) { - std::set result; - if (removals == RemovalBehavior::Delete) { // Removals delete their row, so there is nothing to report. - return result; + return {}; } - Builder::StatementBuilder builder; - builder.Select(s_PUTT_PackageRowId).From(s_PUTT_Table_Name). - Where(s_PUTT_WriteTime).IsGreaterThanOrEqualTo(updateBaseTime). - And(s_PUTT_IsRemoved).Equals(1); + return GetRemovals(connection, s_PUTT_WriteTime, updateBaseTime, false); + } - Statement select = builder.Prepare(connection); + std::set PackageUpdateTrackingTable::GetRemovalsSinceSequence(const SQLite::Connection& connection, int64_t baseSequence, RemovalBehavior removals) + { + THROW_HR_IF(E_NOT_VALID_STATE, removals != RemovalBehavior::Record); - // The rowid is reported rather than the identifier because it is the identity a delta is - // keyed on, and it is the only one that can be compared exactly. Identifiers cannot: the - // casing recorded for a package is frozen when its row is written, so one package can leave - // tombstones under several spellings, and no string comparison available here reproduces - // the ICU LIKE that decides identity elsewhere. Two packages can still vacate the same - // rowid in turn, so the result is a set. - while (select.Step()) + return GetRemovals(connection, s_PUTT_ChangeSequence, baseSequence, true); + } + + int64_t PackageUpdateTrackingTable::GetCurrentChangeSequence(const SQLite::Connection& connection, RemovalBehavior removals) + { + THROW_HR_IF(E_NOT_VALID_STATE, removals != RemovalBehavior::Record); + + if (!Exists(connection)) { - result.emplace(select.GetColumn(0)); + // The table is created on demand, so an index to which no manifest has ever been + // written has none. Nothing has been recorded, so nothing has been sequenced. + return 0; } - return result; + return GetNextChangeSequence(connection) - 1; } SQLite::blob_t PackageUpdateTrackingTable::GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier, RemovalBehavior removals) @@ -555,6 +679,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 packageRowIdBuilder.AlterTable(s_PUTT_Table_Name).Add(Builder::ColumnBuilder(s_PUTT_PackageRowId, Builder::Type::Int64).NotNull().Default(0)); packageRowIdBuilder.Execute(connection); + // Every existing row is backfilled with the same sequence, and the next one issued is 1. + // That is correct rather than merely convenient: an index designated as a baseline + // immediately after migrating records 0 as its own sequence, so a delta against it reports + // only what followed - and what preceded is exactly what the baseline already contains. + Builder::StatementBuilder changeSequenceBuilder; + changeSequenceBuilder.AlterTable(s_PUTT_Table_Name).Add(Builder::ColumnBuilder(s_PUTT_ChangeSequence, Builder::Type::Int64).NotNull().Default(0)); + changeSequenceBuilder.Execute(connection); + // Backfill the rowid for the rows already present. Every one of them is live: 2.0 deletes // the row when a package is removed, so a table being migrated has no tombstones and every // package it names is still in the index. @@ -591,5 +723,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } CreateLiveRowIndex(connection); + CreateChangeSequenceIndex(connection); } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h index 4051180df8..9abc67195e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h @@ -33,6 +33,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Creates the unique index that allows at most one live row per package rowid. static void CreateLiveRowIndex(SQLite::Connection& connection); + // Creates the index over the change sequence. + static void CreateChangeSequenceIndex(SQLite::Connection& connection); + // Creates the table if it does not exist. static void EnsureExists(SQLite::Connection& connection, RemovalBehavior removals); @@ -75,6 +78,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // keyed on and the only one that can be compared exactly; see the implementation. static std::set GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); + // Gets the data on updates written after the given change sequence, exclusive. + // Only available when removals are being recorded, as only then does the column exist. + static std::vector GetUpdatesSinceSequence(const SQLite::Connection& connection, int64_t baseSequence, RemovalBehavior removals); + + // Gets the rowids vacated by packages removed after the given change sequence, exclusive. + // Only available when removals are being recorded, as only then does the column exist. + static std::set GetRemovalsSinceSequence(const SQLite::Connection& connection, int64_t baseSequence, RemovalBehavior removals); + + // Gets the most recently issued change sequence, or 0 if nothing has been written. + // Only available when removals are being recorded, as only then does the column exist. + static int64_t GetCurrentChangeSequence(const SQLite::Connection& connection, RemovalBehavior removals); + // Gets the data hash for the given package identifier. static SQLite::blob_t GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier, RemovalBehavior removals); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h index 472fd7061c..c69e21b408 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h @@ -8,6 +8,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 // The point in time from which the next delta generated against this index should be computed. static constexpr std::string_view s_MetadataValueName_DeltaBaselineTime = "deltaBaselineTime"sv; + // The change sequence from which the next delta generated against this index should be computed. + // This is the value a delta actually uses; the time above is retained for the 2.0 export and for + // diagnostics. A sequence gives an exact, exclusive boundary that whole second times cannot. + static constexpr std::string_view s_MetadataValueName_DeltaBaselineSequence = "deltaBaselineSequence"sv; + // Identifies this index as a baseline that deltas may be generated against. // // The database identifier cannot serve this purpose. An index is prepared from a copy of a diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp index 1b1e837d7d..e4f9fb642b 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -79,11 +79,25 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 // Record the point from which a delta against this index should be computed. Every 2.1 index // does this, because any of them may later be designated as a baseline. - // TODO: We may need to set the baseline time to the max update tracking time +1 to only catch new incoming changes - // This assumes some delay between delta generation and the next package update. // TODO: We also need to ensure that our times are UTC / not impacted by timezone shifts, etc. SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_DeltaBaselineTime, std::to_string(Utility::GetCurrentUnixEpoch())); + // The sequence is what a delta actually uses; the time is retained because it is what the + // 2.0 version data manifest export reads, and because it remains useful diagnostically. + // A sequence is preferred here because the boundary it defines is exact. Whole second times + // cannot separate a change written during the baseline's own second from one written before + // it, so the time based window has to be inclusive and re-carries everything written in that + // second. A sequence is also immune to the clock stepping backwards, which under the time + // scheme silently drops a change and leaves a stale baseline row visible forever. + // + // It has to be recorded rather than recomputed from the baseline later: preparing an index + // drops the tracking table, so a baseline has none to read. That is also why this runs where + // it does, before the drop. Even had the table survived, its maximum is taken over whatever + // rows remain and would fall below the true high water mark once any were removed, so a + // later delta would re-carry changes the baseline already contains. + int64_t currentSequence = V2_0::PackageUpdateTrackingTable::GetCurrentChangeSequence(connection, m_trackingRemovalBehavior); + SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_DeltaBaselineSequence, std::to_string(currentSequence)); + if (!context.Data.Contains(Property::DeltaBaselineIndexPath) || !context.Data.Contains(Property::DeltaOutputPath)) { @@ -97,16 +111,22 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 SQLite::Connection baselineConnection = SQLite::Connection::Create(baselinePath.u8string(), SQLite::Connection::OpenDisposition::ReadOnly); - // The changes to capture are those written after the baseline recorded its own time. - int64_t baselineTime = 0; - std::optional baselineTimeString = SQLite::MetadataTable::TryGetNamedValue(baselineConnection, s_MetadataValueName_DeltaBaselineTime); - if (baselineTimeString && !baselineTimeString->empty()) + // The changes to capture are those written after the baseline recorded its own sequence. + int64_t baselineSequence = 0; + std::optional baselineSequenceString = SQLite::MetadataTable::TryGetNamedValue(baselineConnection, s_MetadataValueName_DeltaBaselineSequence); + if (baselineSequenceString && !baselineSequenceString->empty()) { - baselineTime = std::stoll(baselineTimeString.value()); + baselineSequence = std::stoll(baselineSequenceString.value()); } - auto changedPackages = V2_0::PackageUpdateTrackingTable::GetUpdatesSince(connection, baselineTime, m_trackingRemovalBehavior); - auto removedPackages = V2_0::PackageUpdateTrackingTable::GetRemovalsSince(connection, baselineTime, m_trackingRemovalBehavior); + // A sequence fails in the one direction a time does not: if this index was rebuilt since the + // baseline was taken, its counter restarted below the baseline's value and the window is + // empty. That would produce a silently empty delta, so refuse instead. The equal case is + // legitimate and simply means nothing has changed. + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED, currentSequence < baselineSequence); + + auto changedPackages = V2_0::PackageUpdateTrackingTable::GetUpdatesSinceSequence(connection, baselineSequence, m_trackingRemovalBehavior); + auto removedPackages = V2_0::PackageUpdateTrackingTable::GetRemovalsSinceSequence(connection, baselineSequence, m_trackingRemovalBehavior); Delta::Generate( connection, From 25ba9502583639221f6a14fd50ab4b16e78bbf4f Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 11 Sep 2026 11:42:55 -0700 Subject: [PATCH 34/36] manual review changes --- src/AppInstallerCLITests/SQLiteIndex.cpp | 8 --- src/AppInstallerCLITests/SQLiteIndexDelta.cpp | 58 ++++--------------- 2 files changed, 12 insertions(+), 54 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index 02edf2e04b..713a993018 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -26,9 +26,6 @@ #include #include -#include -#include - using namespace std::string_literals; using namespace std::string_view_literals; using namespace TestCommon; @@ -70,10 +67,6 @@ SQLiteVersion TestPrepareForRead(SQLiteIndex& index) return index.GetVersion(); } - - - - SQLiteIndex SimpleTestSetup(const std::string& filePath, Manifest& manifest, std::optional version = {}) { SQLiteIndex index = CreateTestIndex(filePath, version); @@ -88,7 +81,6 @@ SQLiteIndex SimpleTestSetup(const std::string& filePath, Manifest& manifest, std return index; } - bool ArePackageFamilyNameAndProductCodeSupported(const SQLiteIndex& index, const SQLiteVersion& testVersion) { UNSCOPED_INFO("Index " << index.GetVersion() << " | Test " << testVersion); diff --git a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp index 489874e132..55c5a6cca3 100644 --- a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp +++ b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp @@ -16,15 +16,6 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include - using namespace std::string_literals; using namespace std::string_view_literals; using namespace TestCommon; @@ -85,11 +76,7 @@ namespace return {}; } - // Reads a package's associated values through whatever the given connection calls the tables. - // - // Against a combined connection these names resolve to the merged views, so this reads exactly - // what the 2.0 search path would: the map decides which values a package has, and the value - // table holds the strings. + // Reads a package's associated 1:N values through the given connection. std::set GetOneToManyValues( const Connection& connection, std::string_view tableName, @@ -120,12 +107,6 @@ namespace return GetStrings(connection, sql); } - // Drives the delta workflow, which is otherwise 40 lines of identical ceremony per test. - // - // The shape is fixed by what generation needs: a working index that accumulates changes, a - // designated baseline copied out of it at a chosen point, and a delta produced by preparing the - // working index afterwards. Preparing the working index also leaves it as an ordinary full - // index, which is what the equivalence tests compare the combined form against. struct DeltaTestContext { TempFile WorkingFile{ "delta_working"s, ".db"s }; @@ -171,15 +152,11 @@ namespace } // Opens the working index for the changes that the delta will carry. - // - // The base time reset governs the version data manifest export that preparing performs; the - // window the delta itself uses comes from the baseline's change sequence, and was fixed when - // the baseline was captured. - SQLiteIndex OpenWorkingForChanges() + SQLiteIndex OpenWorkingForChanges(bool resetBaseTimeIfNeeded = true) { SQLiteIndex index = SQLiteIndex::Open(WorkingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - if (!m_baseTimeReset) + if (resetBaseTimeIfNeeded && !m_baseTimeReset) { index.SetProperty(SQLiteIndex::Property::PackageUpdateTrackingBaseTime, ""); m_baseTimeReset = true; @@ -188,18 +165,9 @@ namespace return index; } - void Add(const IndexFields& fields) - { - SQLiteIndex index = OpenWorkingForChanges(); - Manifest manifest = CreateManifest(fields); - index.AddManifest(manifest, fields.Path); - } - - // Adds to the working index without moving the change window, for setup that has to be - // part of the baseline rather than part of the delta. - void AddToWorking(const IndexFields& fields) + void Add(const IndexFields& fields, bool resetBaseTimeIfNeeded = true) { - SQLiteIndex index = SQLiteIndex::Open(WorkingFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + SQLiteIndex index = OpenWorkingForChanges(resetBaseTimeIfNeeded); Manifest manifest = CreateManifest(fields); index.AddManifest(manifest, fields.Path); } @@ -277,8 +245,6 @@ namespace bool m_deltaGenerated = false; }; - // A package with everything the index can store, so that the system reference tables are - // actually populated. The default fake manifest sets none of them. IndexFields MakePackage( std::string id, std::string name, @@ -323,7 +289,7 @@ namespace // --------------------------------------------------------------------------------------------- // Group B - package rowid identity // -// The merged packages view suppresses a baseline row when the delta names the same rowid, and +// The merged package's view suppresses a baseline row when the delta names the same rowid, and // every association refers to a package by that rowid. These cases cover the ways that identity // can be broken. // --------------------------------------------------------------------------------------------- @@ -339,7 +305,7 @@ TEST_CASE("SQLiteIndex_Delta_PackageRemovedThenReAdded", "[sqliteindex][V2_1][de DeltaTestContext context{ { p1, p2, p3 } }; - rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), "Publisher2.Id").value(); + rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), p2.Id).value(); // Removing the middle package frees its rowid, but the next insert takes one above the highest // in use, so the re-add cannot land back on it. @@ -348,7 +314,7 @@ TEST_CASE("SQLiteIndex_Delta_PackageRemovedThenReAdded", "[sqliteindex][V2_1][de context.GenerateDelta(); - rowid_t newRowId = GetPreparedPackageRowId(context.WorkingFile.GetPath(), "Publisher2.Id").value(); + rowid_t newRowId = GetPreparedPackageRowId(context.WorkingFile.GetPath(), p2.Id).value(); REQUIRE(newRowId != originalRowId); { @@ -362,13 +328,13 @@ TEST_CASE("SQLiteIndex_Delta_PackageRemovedThenReAdded", "[sqliteindex][V2_1][de SQLiteIndex combined = context.OpenCombined(); SearchRequest request; - request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "Publisher2.Id")); + request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, p2.Id)); // Exactly one, and from the delta rather than the stale baseline row. auto results = combined.Search(request); REQUIRE(results.Matches.size() == 1); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher2.Id", "Publisher3.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p1.Id, p2.Id, p3.Id }); } // B2. Removing the package that holds the highest rowid frees it for the next package added, so @@ -681,7 +647,7 @@ TEST_CASE("SQLiteIndex_Delta_IdentifierCasingChange_Removed", "[sqliteindex][V2_ DeltaTestContext context; context.CreateWorking({ original, MakePackage("Publisher2.Id", "Package 2") }); - context.AddToWorking(recased); + context.Add(recased, false); context.CaptureBaseline(); REQUIRE(GetPreparedPackageRowId(context.BaselineFile.GetPath(), "publisher1.id").has_value()); @@ -1847,4 +1813,4 @@ TEST_CASE("SQLiteIndex_Delta_SequenceBelowBaselineIsRejected", "[sqliteindex][V2 rebuilt.SetProperty(SQLiteIndex::Property::DeltaOutputPath, rebuiltDeltaFile.GetPath().u8string()); REQUIRE_THROWS_HR(rebuilt.PrepareForPackaging(), APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED); -} \ No newline at end of file +} From ad6bc6b9d14cedab2b5c416fec36229a2bcce7ab Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 11 Sep 2026 13:10:48 -0700 Subject: [PATCH 35/36] Remove duplicated id literals --- src/AppInstallerCLITests/SQLiteIndexDelta.cpp | 289 ++++++++++-------- 1 file changed, 165 insertions(+), 124 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp index 55c5a6cca3..bfeb528136 100644 --- a/src/AppInstallerCLITests/SQLiteIndexDelta.cpp +++ b/src/AppInstallerCLITests/SQLiteIndexDelta.cpp @@ -61,6 +61,11 @@ namespace return result; } + std::set ToStringSet(const std::vector& values) + { + return std::set(values.begin(), values.end()); + } + // Reads the rowid that a prepared index gave a package identifier. std::optional GetPreparedPackageRowId(const std::filesystem::path& indexPath, std::string_view packageIdentifier) { @@ -347,7 +352,7 @@ TEST_CASE("SQLiteIndex_Delta_RemovedPackageRowIdReused", "[sqliteindex][V2_1][de DeltaTestContext context{ { p1, p2, p3 } }; - rowid_t reusedRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), "Publisher3.Id").value(); + rowid_t reusedRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), p3.Id).value(); auto p4 = MakePackage("Publisher4.Id", "Package 4"); @@ -357,7 +362,7 @@ TEST_CASE("SQLiteIndex_Delta_RemovedPackageRowIdReused", "[sqliteindex][V2_1][de REQUIRE_NOTHROW(context.GenerateDelta()); // The new package took the rowid that the removed one gave up. - REQUIRE(GetPreparedPackageRowId(context.WorkingFile.GetPath(), "Publisher4.Id").value() == reusedRowId); + REQUIRE(GetPreparedPackageRowId(context.WorkingFile.GetPath(), p4.Id).value() == reusedRowId); { Connection delta = context.OpenDeltaConnection(); @@ -366,11 +371,11 @@ TEST_CASE("SQLiteIndex_Delta_RemovedPackageRowIdReused", "[sqliteindex][V2_1][de // rowid already displaces the baseline row, and it cannot be written in any case. REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [rowid] = " + std::to_string(reusedRowId)) == 1); REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [rowid] = " + std::to_string(reusedRowId)) == - std::set{ "Publisher4.Id" }); + std::set{ p4.Id }); } SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher2.Id", "Publisher4.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p1.Id, p2.Id, p4.Id }); } // B2 continued. The displaced package's associations must go with it. They are diffed against the @@ -391,12 +396,12 @@ TEST_CASE("SQLiteIndex_Delta_ReusedRowIdReplacesAssociations", "[sqliteindex][V2 Connection merged = context.OpenMergedConnection(); // Nothing of the old occupant survives at the shared rowid. - REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher3.Id") == std::set{ "new1" }); - REQUIRE(GetOneToManyValues(merged, "commands2", "command", "Publisher3.Id") == std::set{ "newcmd" }); - REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", "Publisher3.Id") == std::set{ "newpc" }); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", p3.Id) == ToStringSet(p3.Tags)); + REQUIRE(GetOneToManyValues(merged, "commands2", "command", p3.Id) == ToStringSet(p3.Commands)); + REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", p3.Id) == std::set{ "newpc" }); // The untouched package is unaffected. - REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id") == std::set{ "keep" }); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", p1.Id) == ToStringSet(p1.Tags)); } // B6. Remove, re-add, and remove again leaves a tombstone for each rowid the package vacated. Both @@ -411,7 +416,7 @@ TEST_CASE("SQLiteIndex_Delta_RemoveAddRemove", "[sqliteindex][V2_1][delta]") DeltaTestContext context{ { p1, p2, p3 } }; - rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), "Publisher2.Id").value(); + rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), p2.Id).value(); context.Remove(p2); context.Add(p2); @@ -421,7 +426,7 @@ TEST_CASE("SQLiteIndex_Delta_RemoveAddRemove", "[sqliteindex][V2_1][delta]") Connection working = Connection::Create(context.WorkingFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); // One tombstone per vacated rowid, and generation is told about both of them. - REQUIRE(GetScalar(working, "SELECT COUNT(*) FROM [update_tracking] WHERE [package] = 'Publisher2.Id' AND [is_removed] = 1") == 2); + REQUIRE(GetScalar(working, "SELECT COUNT(*) FROM [update_tracking] WHERE [package] = '" + p2.Id + "' AND [is_removed] = 1") == 2); auto removals = Tracking::GetRemovalsSince(working, 0, Tracking::RemovalBehavior::Record); REQUIRE(removals.size() == 2); @@ -434,12 +439,12 @@ TEST_CASE("SQLiteIndex_Delta_RemoveAddRemove", "[sqliteindex][V2_1][delta]") Connection delta = context.OpenDeltaConnection(); // Only the rowid the baseline actually holds is written; the one the re-add briefly // occupied is above the baseline's range, so there is nothing there to suppress. - REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'Publisher2.Id'") == 1); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = '" + p2.Id + "'") == 1); REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [rowid] = " + std::to_string(originalRowId) + " AND [is_removed] = 1") == 1); } SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher3.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p1.Id, p3.Id }); } // B14. The same sequence, but the re-add changes the casing of the identifier. The tracking table @@ -455,7 +460,7 @@ TEST_CASE("SQLiteIndex_Delta_RemoveAddRemove_CasingChanged", "[sqliteindex][V2_1 DeltaTestContext context{ { p1, p2, p3 } }; - rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), "Publisher2.Id").value(); + rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), p2.Id).value(); // Publisher3 holds the highest rowid, so the re-add lands on a new one and leaves the first // tombstone in place rather than reviving it. @@ -485,7 +490,7 @@ TEST_CASE("SQLiteIndex_Delta_RemoveAddRemove_CasingChanged", "[sqliteindex][V2_1 } SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher3.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p1.Id, p3.Id }); } // B7. When the re-add happens to land on the rowid the package just gave up, the tracking row is @@ -497,7 +502,7 @@ TEST_CASE("SQLiteIndex_Delta_ReAddOnSameRowIdUpdatesInPlace", "[sqliteindex][V2_ DeltaTestContext context{ { p1, p2 } }; - rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), "Publisher2.Id").value(); + rowid_t originalRowId = GetPreparedPackageRowId(context.BaselineFile.GetPath(), p2.Id).value(); // Publisher2 holds the highest rowid, so removing it frees the value that the re-add takes. context.Remove(p2); @@ -505,16 +510,16 @@ TEST_CASE("SQLiteIndex_Delta_ReAddOnSameRowIdUpdatesInPlace", "[sqliteindex][V2_ context.GenerateDelta(); - REQUIRE(GetPreparedPackageRowId(context.WorkingFile.GetPath(), "Publisher2.Id").value() == originalRowId); + REQUIRE(GetPreparedPackageRowId(context.WorkingFile.GetPath(), p2.Id).value() == originalRowId); Connection delta = context.OpenDeltaConnection(); // A single live row, and no tombstone left behind to contradict it. - REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'Publisher2.Id'") == 1); - REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = 'Publisher2.Id' AND [is_removed] = 0") == 1); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = '" + p2.Id + "'") == 1); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] = '" + p2.Id + "' AND [is_removed] = 0") == 1); SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher2.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p1.Id, p2.Id }); } // B8. Two identifiers legitimately share a rowid in the tracking table when one vacates it and the @@ -542,8 +547,8 @@ TEST_CASE("SQLiteIndex_Delta_TrackingAllowsSharedRowIdAcrossPackages", "[sqlitei Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadOnly); - int64_t sharedRowId = GetScalar(connection, "SELECT [package_rowid] FROM [update_tracking] WHERE [package] = 'Publisher2.Id'"); - REQUIRE(GetScalar(connection, "SELECT [package_rowid] FROM [update_tracking] WHERE [package] = 'Publisher1.Id'") == sharedRowId); + int64_t sharedRowId = GetScalar(connection, "SELECT [package_rowid] FROM [update_tracking] WHERE [package] = '" + m2.Manifest.Id + "'"); + REQUIRE(GetScalar(connection, "SELECT [package_rowid] FROM [update_tracking] WHERE [package] = '" + m1.Manifest.Id + "'") == sharedRowId); REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [package_rowid] = " + std::to_string(sharedRowId)) == 2); REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [package_rowid] = " + std::to_string(sharedRowId) + " AND [is_removed] = 0") == 1); @@ -620,12 +625,16 @@ TEST_CASE("SQLiteIndex_Delta_TrackingRowIdMatchesPreparedIndex", "[sqliteindex][ // entire index. TEST_CASE("SQLiteIndex_Delta_IdentifierCasingChange_Changed", "[sqliteindex][V2_1][delta]") { - DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1"), MakePackage("Publisher2.Id", "Package 2") } }; + auto original = MakePackage("Publisher1.Id", "Package 1"); + auto unchanged = MakePackage("Publisher2.Id", "Package 2"); + auto recased = MakePackage("publisher1.id", "Package 1 V2", { "t1", "t2" }, { "c1" }, {}, {}, "2.0"s); + + DeltaTestContext context{ { original, unchanged } }; // Adding a version under a different casing rewrites the ids table entry, and with it the // identifier that packaging will put in the packages table. The tracking row keeps the // original casing. - context.Add(MakePackage("publisher1.id", "Package 1 V2", { "t1", "t2" }, { "c1" }, {}, {}, "2.0"s)); + context.Add(recased); REQUIRE_NOTHROW(context.GenerateDelta()); @@ -633,7 +642,7 @@ TEST_CASE("SQLiteIndex_Delta_IdentifierCasingChange_Changed", "[sqliteindex][V2_ // Exactly one row for the package. Resolving to no rowid would have left the baseline row // unsuppressed alongside the delta's, showing it twice. - REQUIRE(GetSearchedIds(combined) == std::set{ "publisher1.id", "Publisher2.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ recased.Id, unchanged.Id }); } // B13. The silent half of the same defect. Here the casing changed before the baseline was taken, @@ -644,13 +653,14 @@ TEST_CASE("SQLiteIndex_Delta_IdentifierCasingChange_Removed", "[sqliteindex][V2_ { auto original = MakePackage("Publisher1.Id", "Package 1"); auto recased = MakePackage("publisher1.id", "Package 1 V2", { "t1", "t2" }, { "c1" }, {}, {}, "2.0"s); + auto unchanged = MakePackage("Publisher2.Id", "Package 2"); DeltaTestContext context; - context.CreateWorking({ original, MakePackage("Publisher2.Id", "Package 2") }); + context.CreateWorking({ original, unchanged }); context.Add(recased, false); context.CaptureBaseline(); - REQUIRE(GetPreparedPackageRowId(context.BaselineFile.GetPath(), "publisher1.id").has_value()); + REQUIRE(GetPreparedPackageRowId(context.BaselineFile.GetPath(), recased.Id).has_value()); context.Remove(original); context.Remove(recased); @@ -659,7 +669,7 @@ TEST_CASE("SQLiteIndex_Delta_IdentifierCasingChange_Removed", "[sqliteindex][V2_ SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher2.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ unchanged.Id }); } // B11. The migration adds the rowid column to a table whose rows predate it. A backfill that left @@ -694,7 +704,7 @@ TEST_CASE("SQLiteIndex_Delta_TrackingMigrationBackfillsRowIds", "[sqliteindex][V REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [package_rowid] = 0") == 0); // The backfilled value has to be the one the index itself uses, not just any non null. - for (std::string_view packageId : { "Publisher1.Id"sv, "Publisher2.Id"sv }) + for (const auto& packageId : { m1.Manifest.Id, m2.Manifest.Id }) { INFO(packageId); int64_t tracked = GetScalar(connection, "SELECT [package_rowid] FROM [update_tracking] WHERE [package] = '" + std::string{ packageId } + "'"); @@ -756,22 +766,22 @@ TEST_CASE("SQLiteIndex_Delta_RowIdsAreStableAcrossPrepares", "[sqliteindex][V2_0 prepareCopy(third); // Publisher3 is present throughout and must never move. - rowid_t p3First = GetPreparedPackageRowId(first.GetPath(), "Publisher3.Id").value(); - REQUIRE(GetPreparedPackageRowId(second.GetPath(), "Publisher3.Id").value() == p3First); - REQUIRE(GetPreparedPackageRowId(third.GetPath(), "Publisher3.Id").value() == p3First); + rowid_t p3First = GetPreparedPackageRowId(first.GetPath(), manifests[2].Manifest.Id).value(); + REQUIRE(GetPreparedPackageRowId(second.GetPath(), manifests[2].Manifest.Id).value() == p3First); + REQUIRE(GetPreparedPackageRowId(third.GetPath(), manifests[2].Manifest.Id).value() == p3First); // Publisher4 survives from round two to round three. - REQUIRE(GetPreparedPackageRowId(third.GetPath(), "Publisher4.Id").value() == GetPreparedPackageRowId(second.GetPath(), "Publisher4.Id").value()); + REQUIRE(GetPreparedPackageRowId(third.GetPath(), manifests[3].Manifest.Id).value() == GetPreparedPackageRowId(second.GetPath(), manifests[3].Manifest.Id).value()); // B5. A package added after the baseline must land above everything the baseline holds, or it // would collide with an untouched package when the two are merged. rowid_t maxInFirst = 0; - for (std::string_view id : { "Publisher1.Id"sv, "Publisher2.Id"sv, "Publisher3.Id"sv }) + for (const auto& id : { manifests[0].Manifest.Id, manifests[1].Manifest.Id, manifests[2].Manifest.Id }) { maxInFirst = std::max(maxInFirst, GetPreparedPackageRowId(first.GetPath(), id).value()); } - REQUIRE(GetPreparedPackageRowId(second.GetPath(), "Publisher4.Id").value() > maxInFirst); + REQUIRE(GetPreparedPackageRowId(second.GetPath(), manifests[3].Manifest.Id).value() > maxInFirst); } // --------------------------------------------------------------------------------------------- @@ -780,15 +790,18 @@ TEST_CASE("SQLiteIndex_Delta_RowIdsAreStableAcrossPrepares", "[sqliteindex][V2_0 TEST_CASE("SQLiteIndex_Delta_AddedPackage", "[sqliteindex][V2_1][delta]") { - DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); - context.Add(MakePackage("Publisher2.Id", "Package 2")); + DeltaTestContext context{ { p1 } }; + + context.Add(p2); context.GenerateDelta(); Connection delta = context.OpenDeltaConnection(); REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [is_removed] = 0") == 1); - REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 0") == std::set{ "Publisher2.Id" }); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 0") == std::set{ p2.Id }); } TEST_CASE("SQLiteIndex_Delta_RemovedPackage", "[sqliteindex][V2_1][delta]") @@ -804,7 +817,7 @@ TEST_CASE("SQLiteIndex_Delta_RemovedPackage", "[sqliteindex][V2_1][delta]") Connection delta = context.OpenDeltaConnection(); REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [is_removed] = 1") == 1); - REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "Publisher2.Id" }); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ p2.Id }); // A removal carries no data beyond identity, so the rest of the row stays null. REQUIRE(GetScalar(delta, @@ -816,12 +829,11 @@ TEST_CASE("SQLiteIndex_Delta_RemovedPackage", "[sqliteindex][V2_1][delta]") TEST_CASE("SQLiteIndex_Delta_ChangedPackageCopiesEveryColumn", "[sqliteindex][V2_1][delta]") { DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + ManifestAndPath added; + CreateFakeManifestAndPath(added, "Publisher2", "3.4.5", "1.2"sv, "6.7"sv); { SQLiteIndex index = context.OpenWorkingForChanges(); - - ManifestAndPath added; - CreateFakeManifestAndPath(added, "Publisher2", "3.4.5", "1.2"sv, "6.7"sv); index.AddManifest(added.Manifest, added.Path); } @@ -834,8 +846,8 @@ TEST_CASE("SQLiteIndex_Delta_ChangedPackageCopiesEveryColumn", "[sqliteindex][V2 { INFO(column); - auto fromDelta = GetStrings(delta, "SELECT [" + std::string{ column } + "] FROM [delta_packages] WHERE [id] = 'Publisher2.Id'"); - auto fromSource = GetStrings(source, "SELECT [" + std::string{ column } + "] FROM [packages] WHERE [id] = 'Publisher2.Id'"); + auto fromDelta = GetStrings(delta, "SELECT [" + std::string{ column } + "] FROM [delta_packages] WHERE [id] = '" + added.Manifest.Id + "'"); + auto fromSource = GetStrings(source, "SELECT [" + std::string{ column } + "] FROM [packages] WHERE [id] = '" + added.Manifest.Id + "'"); REQUIRE(fromDelta == fromSource); REQUIRE(fromDelta.size() == 1); @@ -843,9 +855,9 @@ TEST_CASE("SQLiteIndex_Delta_ChangedPackageCopiesEveryColumn", "[sqliteindex][V2 } // The hash is a blob, so compare it as one rather than through the string accessor. - Statement deltaHash = Statement::Create(delta, "SELECT [hash] FROM [delta_packages] WHERE [id] = 'Publisher2.Id'"); + Statement deltaHash = Statement::Create(delta, "SELECT [hash] FROM [delta_packages] WHERE [id] = '" + added.Manifest.Id + "'"); REQUIRE(deltaHash.Step()); - Statement sourceHash = Statement::Create(source, "SELECT [hash] FROM [packages] WHERE [id] = 'Publisher2.Id'"); + Statement sourceHash = Statement::Create(source, "SELECT [hash] FROM [packages] WHERE [id] = '" + added.Manifest.Id + "'"); REQUIRE(sourceHash.Step()); auto hashValue = deltaHash.GetColumn(0); @@ -859,45 +871,50 @@ TEST_CASE("SQLiteIndex_Delta_MultipleChangesAndRemovals", "[sqliteindex][V2_1][d auto p2 = MakePackage("Publisher2.Id", "Package 2"); auto p3 = MakePackage("Publisher3.Id", "Package 3"); auto p4 = MakePackage("Publisher4.Id", "Package 4"); + auto p3Updated = MakePackage(p3.Id, "Renamed 3"); + auto p5 = MakePackage("Publisher5.Id", "Package 5"); + auto p6 = MakePackage("Publisher6.Id", "Package 6"); DeltaTestContext context{ { p1, p2, p3, p4 } }; context.Remove(p1); context.Remove(p2); - context.Update(MakePackage("Publisher3.Id", "Renamed 3")); - context.Add(MakePackage("Publisher5.Id", "Package 5")); - context.Add(MakePackage("Publisher6.Id", "Package 6")); + context.Update(p3Updated); + context.Add(p5); + context.Add(p6); context.GenerateDelta(); Connection delta = context.OpenDeltaConnection(); REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == - std::set{ "Publisher1.Id", "Publisher2.Id" }); + std::set{ p1.Id, p2.Id }); REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 0") == - std::set{ "Publisher3.Id", "Publisher5.Id", "Publisher6.Id" }); + std::set{ p3Updated.Id, p5.Id, p6.Id }); SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher3.Id", "Publisher4.Id", "Publisher5.Id", "Publisher6.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p3Updated.Id, p4.Id, p5.Id, p6.Id }); } // C6. A package that came and went within the window was never in the baseline, so there is // nothing to suppress and the delta must not claim to remove it. TEST_CASE("SQLiteIndex_Delta_PackageAddedAndRemovedWithinWindow", "[sqliteindex][V2_1][delta]") { - DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; - + auto p1 = MakePackage("Publisher1.Id", "Package 1"); auto transient = MakePackage("Transient.Id", "Transient"); + + DeltaTestContext context{ { p1 } }; + context.Add(transient); context.Remove(transient); REQUIRE_NOTHROW(context.GenerateDelta()); Connection delta = context.OpenDeltaConnection(); - REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] LIKE 'Transient.Id'") == 0); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_packages] WHERE [id] LIKE '" + transient.Id + "'") == 0); SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p1.Id }); } // C8. The identifier lookup used to be a LIKE, which treats these characters as wildcards. A @@ -916,15 +933,17 @@ TEST_CASE("SQLiteIndex_Delta_IdentifierWithLikeWildcards", "[sqliteindex][V2_1][ Connection delta = context.OpenDeltaConnection(); // Only the package actually removed; a LIKE would also have matched the decoy. - REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ "Publisher_A.Id" }); + REQUIRE(GetStrings(delta, "SELECT [id] FROM [delta_packages] WHERE [is_removed] = 1") == std::set{ wild.Id }); SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "PublisherXA.Id", "Pub%cent.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ decoy.Id, percent.Id }); } TEST_CASE("SQLiteIndex_Delta_NoChanges_EmptyDelta", "[sqliteindex][V2_1][delta]") { - DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + + DeltaTestContext context{ { p1 } }; // Reset the base time without making any change, so nothing is reported. context.OpenWorkingForChanges(); @@ -960,7 +979,7 @@ TEST_CASE("SQLiteIndex_Delta_NoChanges_EmptyDelta", "[sqliteindex][V2_1][delta]" // An empty delta still has to merge cleanly. SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p1.Id }); } // --------------------------------------------------------------------------------------------- @@ -973,11 +992,12 @@ TEST_CASE("SQLiteIndex_Delta_SystemReference_AddAndRemove", "[sqliteindex][V2_1] { auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, { "Family1_8wekyb3d8bbwe" }, { "PC-KEEP", "PC-DROP" }); auto p2 = MakePackage("Publisher2.Id", "Package 2", { "t1" }, { "c1" }, { "Family2_8wekyb3d8bbwe" }, { "PC-OTHER" }); + auto p1Updated = MakePackage(p1.Id, p1.Name, { "t1" }, { "c1" }, { "Family3_8wekyb3d8bbwe" }, { "PC-KEEP", "PC-NEW" }); DeltaTestContext context{ { p1, p2 } }; // Trade one product code for another while keeping a third, and swap the family name. - context.Update(MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, { "Family3_8wekyb3d8bbwe" }, { "PC-KEEP", "PC-NEW" })); + context.Update(p1Updated); context.GenerateDelta(); @@ -999,13 +1019,13 @@ TEST_CASE("SQLiteIndex_Delta_SystemReference_AddAndRemove", "[sqliteindex][V2_1] Connection merged = context.OpenMergedConnection(); // D3. Suppression is per row: the kept code survives even though the package changed. - REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", "Publisher1.Id") == + REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", p1Updated.Id) == std::set{ "pc-keep", "pc-new" }); - REQUIRE(GetSystemReferenceValues(merged, "pfns2", "pfn", "Publisher1.Id") == + REQUIRE(GetSystemReferenceValues(merged, "pfns2", "pfn", p1Updated.Id) == std::set{ "family3_8wekyb3d8bbwe" }); // The untouched package keeps everything. - REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", "Publisher2.Id") == + REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", p2.Id) == std::set{ "pc-other" }); } @@ -1013,47 +1033,52 @@ TEST_CASE("SQLiteIndex_Delta_SystemReference_AddAndRemove", "[sqliteindex][V2_1] TEST_CASE("SQLiteIndex_Delta_SystemReference_CorrelationThroughCombined", "[sqliteindex][V2_1][delta]") { auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, { "Family1_8wekyb3d8bbwe" }, { "PC-KEEP", "PC-DROP" }); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); + auto p1Updated = MakePackage(p1.Id, p1.Name, { "t1" }, { "c1" }, p1.PackageFamilyNames, { "PC-KEEP", "PC-NEW" }); - DeltaTestContext context{ { p1, MakePackage("Publisher2.Id", "Package 2") } }; + DeltaTestContext context{ { p1, p2 } }; - context.Update(MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, { "Family1_8wekyb3d8bbwe" }, { "PC-KEEP", "PC-NEW" })); + context.Update(p1Updated); context.GenerateDelta(); SQLiteIndex combined = context.OpenCombined(); - for (std::string_view productCode : { "PC-KEEP"sv, "PC-NEW"sv }) + for (const auto& productCode : p1Updated.ProductCodes) { INFO(productCode); SearchRequest request; request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, std::string{ productCode })); - REQUIRE(GetSearchedIds(combined, request) == std::set{ "Publisher1.Id" }); + REQUIRE(GetSearchedIds(combined, request) == std::set{ p1Updated.Id }); } // The dropped code must no longer correlate to anything. SearchRequest dropped; - dropped.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, "PC-DROP"s)); + dropped.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, p1.ProductCodes[1])); REQUIRE(combined.Search(dropped).Matches.empty()); // And the family name still resolves through the merged view. SearchRequest family; - family.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::PackageFamilyName, MatchType::Exact, "Family1_8wekyb3d8bbwe"s)); - REQUIRE(GetSearchedIds(combined, family) == std::set{ "Publisher1.Id" }); + family.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::PackageFamilyName, MatchType::Exact, p1Updated.PackageFamilyNames[0])); + REQUIRE(GetSearchedIds(combined, family) == std::set{ p1Updated.Id }); } // D5. The normalized name and publisher tables are populated as a side effect of the package name, // so a rename has to move them. Nothing asserted on them before. TEST_CASE("SQLiteIndex_Delta_SystemReference_NormalizedNameFollowsRename", "[sqliteindex][V2_1][delta]") { - DeltaTestContext context{ { MakePackage("Publisher1.Id", "Original Name") } }; + auto original = MakePackage("Publisher1.Id", "Original Name"); + auto renamed = MakePackage(original.Id, "Replacement Name"); + + DeltaTestContext context{ { original } }; - context.Update(MakePackage("Publisher1.Id", "Replacement Name")); + context.Update(renamed); context.GenerateDelta(); Connection merged = context.OpenMergedConnection(); - auto names = GetSystemReferenceValues(merged, "norm_names2", "norm_name", "Publisher1.Id"); + auto names = GetSystemReferenceValues(merged, "norm_names2", "norm_name", renamed.Id); REQUIRE(names.size() == 1); // The old name must be gone rather than merely joined by the new one. @@ -1066,11 +1091,11 @@ TEST_CASE("SQLiteIndex_Delta_SystemReference_NormalizedNameFollowsRename", "[sql // exactly what a full index built from the same data holds. Connection reference = Connection::Create(context.WorkingFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); - REQUIRE(GetSystemReferenceValues(merged, "norm_names2", "norm_name", "Publisher1.Id") == - GetSystemReferenceValues(reference, "norm_names2", "norm_name", "Publisher1.Id")); + REQUIRE(GetSystemReferenceValues(merged, "norm_names2", "norm_name", renamed.Id) == + GetSystemReferenceValues(reference, "norm_names2", "norm_name", renamed.Id)); - REQUIRE(GetSystemReferenceValues(merged, "norm_publishers2", "norm_publisher", "Publisher1.Id") == - GetSystemReferenceValues(reference, "norm_publishers2", "norm_publisher", "Publisher1.Id")); + REQUIRE(GetSystemReferenceValues(merged, "norm_publishers2", "norm_publisher", renamed.Id) == + GetSystemReferenceValues(reference, "norm_publishers2", "norm_publisher", renamed.Id)); } TEST_CASE("SQLiteIndex_Delta_SystemReference_RemovedPackageValuesAreInvisible", "[sqliteindex][V2_1][delta]") @@ -1094,14 +1119,14 @@ TEST_CASE("SQLiteIndex_Delta_SystemReference_RemovedPackageValuesAreInvisible", Connection merged = context.OpenMergedConnection(); - REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", "Publisher2.Id").empty()); - REQUIRE(GetSystemReferenceValues(merged, "pfns2", "pfn", "Publisher2.Id").empty()); - REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", "Publisher1.Id") == std::set{ "pc-1" }); + REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", p2.Id).empty()); + REQUIRE(GetSystemReferenceValues(merged, "pfns2", "pfn", p2.Id).empty()); + REQUIRE(GetSystemReferenceValues(merged, "productcodes2", "productcode", p1.Id) == std::set{ "pc-1" }); SQLiteIndex combined = context.OpenCombined(); SearchRequest request; - request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, "PC-2"s)); + request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, p2.ProductCodes[0])); REQUIRE(combined.Search(request).Matches.empty()); } @@ -1115,24 +1140,25 @@ TEST_CASE("SQLiteIndex_Delta_OneToMany_AssociationsAreSuppressedPerRow", "[sqlit { auto p1 = MakePackage("Publisher1.Id", "Package 1", { "keep", "drop", "alsokeep" }, { "cmdkeep", "cmddrop" }); auto p2 = MakePackage("Publisher2.Id", "Package 2", { "other" }, { "othercmd" }); + auto p1Updated = MakePackage(p1.Id, p1.Name, { "keep", "added", "alsokeep" }, { "cmdkeep", "cmdadded" }); DeltaTestContext context{ { p1, p2 } }; - context.Update(MakePackage("Publisher1.Id", "Package 1", { "keep", "added", "alsokeep" }, { "cmdkeep", "cmdadded" })); + context.Update(p1Updated); context.Remove(p2); context.GenerateDelta(); Connection merged = context.OpenMergedConnection(); - REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id") == std::set{ "keep", "added", "alsokeep" }); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", p1Updated.Id) == ToStringSet(p1Updated.Tags)); // E2. The same defect on the other one to many table. - REQUIRE(GetOneToManyValues(merged, "commands2", "command", "Publisher1.Id") == std::set{ "cmdkeep", "cmdadded" }); + REQUIRE(GetOneToManyValues(merged, "commands2", "command", p1Updated.Id) == ToStringSet(p1Updated.Commands)); // F3 again, for the map tables. - REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher2.Id").empty()); - REQUIRE(GetOneToManyValues(merged, "commands2", "command", "Publisher2.Id").empty()); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", p2.Id).empty()); + REQUIRE(GetOneToManyValues(merged, "commands2", "command", p2.Id).empty()); } // E3/E4. A value the baseline already knows is referenced at its existing rowid rather than copied, @@ -1141,11 +1167,12 @@ TEST_CASE("SQLiteIndex_Delta_OneToMany_ValueRowIdAllocation", "[sqliteindex][V2_ { auto p1 = MakePackage("Publisher1.Id", "Package 1", { "shared", "only1" }); auto p2 = MakePackage("Publisher2.Id", "Package 2", { "other" }); + auto p2Updated = MakePackage(p2.Id, p2.Name, { "other", "shared", "brandnew" }); DeltaTestContext context{ { p1, p2 } }; // Publisher2 gains a tag the baseline already has, plus one it does not. - context.Update(MakePackage("Publisher2.Id", "Package 2", { "other", "shared", "brandnew" })); + context.Update(p2Updated); context.GenerateDelta(); rowid_t baselineMaxTagRowId = 0; @@ -1154,23 +1181,23 @@ TEST_CASE("SQLiteIndex_Delta_OneToMany_ValueRowIdAllocation", "[sqliteindex][V2_ { Connection baseline = Connection::Create(context.BaselineFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); baselineMaxTagRowId = static_cast(GetScalar(baseline, "SELECT MAX([rowid]) FROM [tags2]")); - sharedRowIdInBaseline = static_cast(GetScalar(baseline, "SELECT [rowid] FROM [tags2] WHERE [tag] = 'shared'")); + sharedRowIdInBaseline = static_cast(GetScalar(baseline, "SELECT [rowid] FROM [tags2] WHERE [tag] = '" + p1.Tags[0] + "'")); } Connection delta = context.OpenDeltaConnection(); // E4. The shared value is not copied into the delta; the map points at the baseline rowid. - REQUIRE(GetStrings(delta, "SELECT [tag] FROM [delta_tags2]") == std::set{ "brandnew" }); + REQUIRE(GetStrings(delta, "SELECT [tag] FROM [delta_tags2]") == std::set{ p2Updated.Tags[2] }); auto mapTable = Delta::GetMapTableName("tags2"); REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [" + mapTable + "] WHERE [tag] = " + std::to_string(sharedRowIdInBaseline) + " AND [is_removed] = 0") == 1); // E3. The new value is numbered above the baseline, so the union cannot collide. - REQUIRE(GetScalar(delta, "SELECT [rowid] FROM [delta_tags2] WHERE [tag] = 'brandnew'") > baselineMaxTagRowId); + REQUIRE(GetScalar(delta, "SELECT [rowid] FROM [delta_tags2] WHERE [tag] = '" + p2Updated.Tags[2] + "'") > baselineMaxTagRowId); // F4. Both sides resolve through the unioned value view. Connection merged = context.OpenMergedConnection(); - REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher2.Id") == std::set{ "other", "shared", "brandnew" }); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", p2Updated.Id) == ToStringSet(p2Updated.Tags)); } // E5. One new value shared by two packages is stored once and mapped twice. @@ -1178,35 +1205,39 @@ TEST_CASE("SQLiteIndex_Delta_OneToMany_NewValueSharedByPackages", "[sqliteindex] { auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1" }); auto p2 = MakePackage("Publisher2.Id", "Package 2", { "t2" }); + auto p1Updated = MakePackage(p1.Id, p1.Name, { "t1", "commontag" }); + auto p2Updated = MakePackage(p2.Id, p2.Name, { "t2", "commontag" }); DeltaTestContext context{ { p1, p2 } }; - context.Update(MakePackage("Publisher1.Id", "Package 1", { "t1", "commontag" })); - context.Update(MakePackage("Publisher2.Id", "Package 2", { "t2", "commontag" })); + context.Update(p1Updated); + context.Update(p2Updated); context.GenerateDelta(); Connection delta = context.OpenDeltaConnection(); - REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_tags2] WHERE [tag] = 'commontag'") == 1); + REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [delta_tags2] WHERE [tag] = '" + p1Updated.Tags[1] + "'") == 1); - rowid_t valueRowId = static_cast(GetScalar(delta, "SELECT [rowid] FROM [delta_tags2] WHERE [tag] = 'commontag'")); + rowid_t valueRowId = static_cast(GetScalar(delta, "SELECT [rowid] FROM [delta_tags2] WHERE [tag] = '" + p1Updated.Tags[1] + "'")); auto mapTable = Delta::GetMapTableName("tags2"); REQUIRE(GetScalar(delta, "SELECT COUNT(*) FROM [" + mapTable + "] WHERE [tag] = " + std::to_string(valueRowId) + " AND [is_removed] = 0") == 2); Connection merged = context.OpenMergedConnection(); - REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id") == std::set{ "t1", "commontag" }); - REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher2.Id") == std::set{ "t2", "commontag" }); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", p1Updated.Id) == ToStringSet(p1Updated.Tags)); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", p2Updated.Id) == ToStringSet(p2Updated.Tags)); } // E6. Every value removed from a package. An empty current set must not read as "nothing changed". TEST_CASE("SQLiteIndex_Delta_OneToMany_AllValuesRemoved", "[sqliteindex][V2_1][delta]") { auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1", "t2" }, { "c1" }); + auto p2 = MakePackage("Publisher2.Id", "Package 2", { "t3" }, { "c2" }); + auto p1Updated = MakePackage(p1.Id, p1.Name, {}, {}); - DeltaTestContext context{ { p1, MakePackage("Publisher2.Id", "Package 2", { "t3" }, { "c2" }) } }; + DeltaTestContext context{ { p1, p2 } }; - context.Update(MakePackage("Publisher1.Id", "Package 1", {}, {})); + context.Update(p1Updated); context.GenerateDelta(); { @@ -1216,33 +1247,36 @@ TEST_CASE("SQLiteIndex_Delta_OneToMany_AllValuesRemoved", "[sqliteindex][V2_1][d } Connection merged = context.OpenMergedConnection(); - REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id").empty()); - REQUIRE(GetOneToManyValues(merged, "commands2", "command", "Publisher1.Id").empty()); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", p1Updated.Id).empty()); + REQUIRE(GetOneToManyValues(merged, "commands2", "command", p1Updated.Id).empty()); // The other package is untouched. - REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher2.Id") == std::set{ "t3" }); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", p2.Id) == ToStringSet(p2.Tags)); } // E7. A baseline with no values at all, so the maximum rowid query has nothing to report. TEST_CASE("SQLiteIndex_Delta_OneToMany_EmptyBaselineValueTable", "[sqliteindex][V2_1][delta]") { - DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1", {}, {}) } }; + auto p1 = MakePackage("Publisher1.Id", "Package 1", {}, {}); + auto p1Updated = MakePackage(p1.Id, p1.Name, { "first" }, { "firstcmd" }); + + DeltaTestContext context{ { p1 } }; { Connection baseline = Connection::Create(context.BaselineFile.GetPath().u8string(), Connection::OpenDisposition::ReadOnly); REQUIRE(GetRowCount(baseline, "tags2") == 0); } - context.Update(MakePackage("Publisher1.Id", "Package 1", { "first" }, { "firstcmd" })); + context.Update(p1Updated); REQUIRE_NOTHROW(context.GenerateDelta()); Connection delta = context.OpenDeltaConnection(); // Numbering has to start somewhere valid; rowid 0 is not. - REQUIRE(GetScalar(delta, "SELECT [rowid] FROM [delta_tags2] WHERE [tag] = 'first'") > 0); + REQUIRE(GetScalar(delta, "SELECT [rowid] FROM [delta_tags2] WHERE [tag] = '" + p1Updated.Tags[0] + "'") > 0); Connection merged = context.OpenMergedConnection(); - REQUIRE(GetOneToManyValues(merged, "tags2", "tag", "Publisher1.Id") == std::set{ "first" }); + REQUIRE(GetOneToManyValues(merged, "tags2", "tag", p1Updated.Id) == ToStringSet(p1Updated.Tags)); } // --------------------------------------------------------------------------------------------- @@ -1279,14 +1313,16 @@ TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_Search", "[sqliteindex][V2_1][delt // a URI rather than a plain path, so both files have to be named that way for the attach to // resolve at all. auto disposition = GENERATE(SQLiteStorageBase::OpenDisposition::Read, SQLiteStorageBase::OpenDisposition::Immutable); + auto p1 = MakePackage("Publisher1.Id", "Package 1"); + auto p2 = MakePackage("Publisher2.Id", "Package 2"); - DeltaTestContext context{ { MakePackage("Publisher1.Id", "Package 1") } }; + DeltaTestContext context{ { p1 } }; - context.Add(MakePackage("Publisher2.Id", "Package 2")); + context.Add(p2); context.GenerateDelta(); SQLiteIndex combined = context.OpenCombined(disposition); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher2.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p1.Id, p2.Id }); } TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_RemovedPackageExcluded", "[sqliteindex][V2_1][delta]") @@ -1300,7 +1336,7 @@ TEST_CASE("SQLiteIndex_Delta_OpenWithBaseline_RemovedPackageExcluded", "[sqlitei context.GenerateDelta(); SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p1.Id }); } TEST_CASE("SQLiteIndex_Delta_UnmarkedBaselineRejected", "[sqliteindex][V2_1][delta]") @@ -1441,7 +1477,7 @@ TEST_CASE("SQLiteIndex_Delta_CheckConsistency_ReAddedPackageIsNotCorruption", "[ REQUIRE(index.CheckConsistency(true)); Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadOnly); - REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [package] = 'Publisher2.Id'") == 2); + REQUIRE(GetScalar(connection, "SELECT COUNT(*) FROM [update_tracking] WHERE [package] = '" + m2.Manifest.Id + "'") == 2); // The removal is still reported, since the old rowid genuinely was vacated. auto removals = Tracking::GetRemovalsSince(connection, 0, Tracking::RemovalBehavior::Record); @@ -1449,17 +1485,18 @@ TEST_CASE("SQLiteIndex_Delta_CheckConsistency_ReAddedPackageIsNotCorruption", "[ // And it is not also reported as an update under that identity being gone. auto updates = Tracking::GetUpdatesSince(connection, 0, Tracking::RemovalBehavior::Record); - REQUIRE(std::count_if(updates.begin(), updates.end(), [](const auto& u) { return u.PackageIdentifier == "Publisher2.Id"; }) == 1); + REQUIRE(std::count_if(updates.begin(), updates.end(), [&](const auto& u) { return u.PackageIdentifier == m2.Manifest.Id; }) == 1); } TEST_CASE("SQLiteIndex_Delta_CheckConsistency_OnCombinedIndex", "[sqliteindex][V2_1][delta]") { auto p1 = MakePackage("Publisher1.Id", "Package 1", { "t1" }, { "c1" }, {}, { "PC-1" }); auto p2 = MakePackage("Publisher2.Id", "Package 2", { "t2" }, { "c2" }, {}, { "PC-2" }); + auto p1Updated = MakePackage(p1.Id, p1.Name, { "t1", "t3" }, p1.Commands, p1.PackageFamilyNames, p1.ProductCodes); DeltaTestContext context{ { p1, p2 } }; - context.Update(MakePackage("Publisher1.Id", "Package 1", { "t1", "t3" }, { "c1" }, {}, { "PC-1" })); + context.Update(p1Updated); context.Remove(p2); context.GenerateDelta(); @@ -1624,13 +1661,16 @@ TEST_CASE("SQLiteIndex_Delta_EquivalenceWithFullIndex", "[sqliteindex][V2_1][del auto retagged = MakePackage("Equivalence.Retagged", "Package Retagged", { "shared", "changed" }, { "cmdold" }, { "Family2_8wekyb3d8bbwe" }, { "PC-OLD", "PC-BOTH" }); auto renamed = MakePackage("Equivalence.Renamed", "Package Original", { "shared" }, { "cmdkeep" }, { "Family3_8wekyb3d8bbwe" }, { "PC-RENAMED" }); auto roundTrip = MakePackage("Equivalence.RoundTrip", "Package RoundTrip", { "shared" }, { "cmdkeep" }, {}, { "PC-ROUND" }); + auto retaggedUpdated = MakePackage(retagged.Id, retagged.Name, { "shared", "changednew" }, { "cmdnew" }, retagged.PackageFamilyNames, { "PC-NEW", "PC-BOTH" }); + auto renamedUpdated = MakePackage(renamed.Id, "Package Replacement", renamed.Tags, renamed.Commands, renamed.PackageFamilyNames, renamed.ProductCodes); + auto added = MakePackage("Equivalence.Added", "Package Added", { "shared", "brand" }, { "cmdadded" }, { "Family4_8wekyb3d8bbwe" }, { "PC-ADDED" }); DeltaTestContext context{ { untouched, removed, retagged, renamed, roundTrip } }; context.Remove(removed); - context.Update(MakePackage("Equivalence.Retagged", "Package Retagged", { "shared", "changednew" }, { "cmdnew" }, { "Family2_8wekyb3d8bbwe" }, { "PC-NEW", "PC-BOTH" })); - context.Update(MakePackage("Equivalence.Renamed", "Package Replacement", { "shared" }, { "cmdkeep" }, { "Family3_8wekyb3d8bbwe" }, { "PC-RENAMED" })); - context.Add(MakePackage("Equivalence.Added", "Package Added", { "shared", "brand" }, { "cmdadded" }, { "Family4_8wekyb3d8bbwe" }, { "PC-ADDED" })); + context.Update(retaggedUpdated); + context.Update(renamedUpdated); + context.Add(added); // A removal and re-add in the same window, which is where rowid identity is hardest. context.Remove(roundTrip); @@ -1642,7 +1682,7 @@ TEST_CASE("SQLiteIndex_Delta_EquivalenceWithFullIndex", "[sqliteindex][V2_1][del SQLiteIndex full = context.OpenFullIndex(); REQUIRE(GetSearchedIds(full) == std::set{ - "Equivalence.Untouched", "Equivalence.Retagged", "Equivalence.Renamed", "Equivalence.RoundTrip", "Equivalence.Added" }); + untouched.Id, retaggedUpdated.Id, renamedUpdated.Id, roundTrip.Id, added.Id }); RequireEquivalent(combined, full); } @@ -1682,6 +1722,7 @@ TEST_CASE("SQLiteIndex_Delta_ChangeSequenceAdvancesOnEveryWrite", "[sqliteindex] auto p1 = MakePackage("Publisher1.Id", "Package 1"); auto p2 = MakePackage("Publisher2.Id", "Package 2"); + auto p1Updated = MakePackage(p1.Id, "Package 1 Renamed"); { SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, s_DeltaVersion); @@ -1693,7 +1734,7 @@ TEST_CASE("SQLiteIndex_Delta_ChangeSequenceAdvancesOnEveryWrite", "[sqliteindex] Manifest m2 = CreateManifest(p2); index.AddManifest(m2, p2.Path); - Manifest m1Updated = CreateManifest(MakePackage("Publisher1.Id", "Package 1 Renamed")); + Manifest m1Updated = CreateManifest(p1Updated); REQUIRE(index.UpdateManifest(m1Updated, p1.Path)); } @@ -1704,8 +1745,8 @@ TEST_CASE("SQLiteIndex_Delta_ChangeSequenceAdvancesOnEveryWrite", "[sqliteindex] REQUIRE(GetRowCount(connection, "update_tracking") == 2); REQUIRE(GetScalar(connection, "SELECT MAX([change_seq]) FROM [update_tracking]") == 3); - int64_t first = GetScalar(connection, "SELECT [change_seq] FROM [update_tracking] WHERE [package] = 'Publisher1.Id'"); - int64_t second = GetScalar(connection, "SELECT [change_seq] FROM [update_tracking] WHERE [package] = 'Publisher2.Id'"); + int64_t first = GetScalar(connection, "SELECT [change_seq] FROM [update_tracking] WHERE [package] = '" + p1Updated.Id + "'"); + int64_t second = GetScalar(connection, "SELECT [change_seq] FROM [update_tracking] WHERE [package] = '" + p2.Id + "'"); // The updated package is now the more recent of the two, having started as the older. REQUIRE(first == 3); @@ -1736,7 +1777,7 @@ TEST_CASE("SQLiteIndex_Delta_BaselineCapturedImmediatelyExcludesItsOwnData", "[s REQUIRE(GetRowCount(delta, "delta_packages") == 0); SQLiteIndex combined = context.OpenCombined(); - REQUIRE(GetSearchedIds(combined) == std::set{ "Publisher1.Id", "Publisher2.Id" }); + REQUIRE(GetSearchedIds(combined) == std::set{ p1.Id, p2.Id }); } // K3. A migrated table has no sequences, so every row backfills to the same value and the first @@ -1750,6 +1791,8 @@ TEST_CASE("SQLiteIndex_Delta_TrackingMigrationBackfillsChangeSequence", "[sqlite CreateFakeManifestAndPath(m1, "Publisher1", "1.0"); ManifestAndPath m2; CreateFakeManifestAndPath(m2, "Publisher2", "1.0"); + ManifestAndPath m3; + CreateFakeManifestAndPath(m3, "Publisher3", "1.0"); { SQLiteIndex index = SQLiteIndex::CreateNew(indexFile, SQLiteVersion{ 2, 0 }); @@ -1772,13 +1815,11 @@ TEST_CASE("SQLiteIndex_Delta_TrackingMigrationBackfillsChangeSequence", "[sqlite // A write after the migration has to be distinguishable from everything that preceded it. { SQLiteIndex index = SQLiteIndex::Open(indexFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - ManifestAndPath m3; - CreateFakeManifestAndPath(m3, "Publisher3", "1.0"); index.AddManifest(m3.Manifest, m3.Path); } Connection connection = Connection::Create(indexFile, Connection::OpenDisposition::ReadOnly); - REQUIRE(GetScalar(connection, "SELECT [change_seq] FROM [update_tracking] WHERE [package] = 'Publisher3.Id'") == 1); + REQUIRE(GetScalar(connection, "SELECT [change_seq] FROM [update_tracking] WHERE [package] = '" + m3.Manifest.Id + "'") == 1); } // K4. The one direction a sequence fails in that a time does not. If the working index is rebuilt, From 3797c414c534aeaf624359097f541c97d9bd1354 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 11 Sep 2026 16:56:39 -0700 Subject: [PATCH 36/36] Manual review complete --- .../AppInstallerCLITests.vcxproj.filters | 12 ++++ .../SQLiteIndexTestCommon.cpp | 6 +- .../SQLiteIndexTestCommon.h | 12 +--- .../Microsoft/SQLiteIndex.cpp | 6 +- .../Microsoft/Schema/2_0/Interface.h | 6 +- .../Microsoft/Schema/2_0/Interface_2_0.cpp | 3 +- .../Schema/2_0/PackageUpdateTrackingTable.cpp | 63 ++----------------- .../Schema/2_0/PackageUpdateTrackingTable.h | 16 +---- .../Microsoft/Schema/2_0/PackagesTable.cpp | 32 +++------- .../Microsoft/Schema/2_0/PackagesTable.h | 5 +- .../Microsoft/Schema/2_1/DeltaGeneration.cpp | 28 +-------- .../Microsoft/Schema/2_1/DeltaTables.cpp | 15 +---- .../Microsoft/Schema/2_1/DeltaViews.cpp | 13 +--- .../Microsoft/Schema/2_1/DeltaViews.h | 2 - .../Microsoft/Schema/2_1/Interface.h | 11 +--- .../Microsoft/Schema/2_1/Interface_2_1.cpp | 36 +++-------- .../Microsoft/Schema/ISQLiteIndex.cpp | 3 +- .../Microsoft/Schema/ISQLiteIndex.h | 4 +- 18 files changed, 51 insertions(+), 222 deletions(-) diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 5fc2e79a90..d187f219e1 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -99,6 +99,9 @@ Header Files + + Header Files + @@ -419,6 +422,15 @@ Source Files\Repository + + Source Files + + + Source Files\Repository + + + Source Files\Repository + diff --git a/src/AppInstallerCLITests/SQLiteIndexTestCommon.cpp b/src/AppInstallerCLITests/SQLiteIndexTestCommon.cpp index e9d9a566aa..83b44015c0 100644 --- a/src/AppInstallerCLITests/SQLiteIndexTestCommon.cpp +++ b/src/AppInstallerCLITests/SQLiteIndexTestCommon.cpp @@ -21,9 +21,9 @@ namespace TestCommon // If no specific version requested, then use generator to run against the last 3 versions. if (!version) { - SQLiteVersion latestVersion{ 2, 0 }; - SQLiteVersion versionMinus1 = SQLiteVersion{ 1, 7 }; - SQLiteVersion versionMinus2 = SQLiteVersion{ 1, 6 }; + SQLiteVersion latestVersion{ 2, 1 }; + SQLiteVersion versionMinus1 = SQLiteVersion{ 2, 0 }; + SQLiteVersion versionMinus2 = SQLiteVersion{ 1, 7 }; version = GENERATE_COPY(SQLiteVersion{ versionMinus2 }, SQLiteVersion{ versionMinus1 }, SQLiteVersion{ latestVersion }); } diff --git a/src/AppInstallerCLITests/SQLiteIndexTestCommon.h b/src/AppInstallerCLITests/SQLiteIndexTestCommon.h index 7d50093739..56d25cd702 100644 --- a/src/AppInstallerCLITests/SQLiteIndexTestCommon.h +++ b/src/AppInstallerCLITests/SQLiteIndexTestCommon.h @@ -5,15 +5,8 @@ #include #include -#include -#include -#include -#include -#include - -// Fixture helpers shared by the index test files. They live here rather than in TestCommon so that -// only the tests that build indexes pay for the manifest and index headers. +// Fixture helpers shared by the index test files. namespace TestCommon { using SQLiteVersion = AppInstaller::SQLite::Version; @@ -169,9 +162,6 @@ namespace TestCommon void ApplyIndexFields(AppInstaller::Manifest::Manifest& manifest, const IndexFields& fields); // Produces the manifest described by the given fields. - // - // Unlike ApplyIndexFields, nothing can carry over from a previously described package, which is - // what a test that replaces a package's data wants. AppInstaller::Manifest::Manifest CreateManifest(const IndexFields& fields); // Creates an index containing the given data. diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp index 30aca55a70..10d7a5699c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -58,11 +58,7 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex result{ SQLite::DatabaseSpecifier{ deltaFilePath, disposition }, {} }; - result.m_contextData.Add(baselinePath); - - // The interface for the delta's schema version establishes the combined view. A version - // that does not understand deltas throws, which is the right answer: nothing else here - // could make sense of the pair. + // The interface for the delta's schema version establishes the combined view. result.m_interface->SetupDeltaReadMode(result.m_dbconn, SQLite::DatabaseSpecifier{ baselineFilePath, disposition }); return result; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h index 4532c942c7..f74a86bf5b 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface.h @@ -97,14 +97,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 mutable bool m_internalInterfaceChecked = false; // Determines how the removal of a package is recorded in the update tracking table. - // Version 2.0 deletes the row; a derived version sets this to record the removal instead, - // so that a delta index can express it. This varies only by schema version, so the - // constructor of that version establishes it rather than a virtual answering per call. PackageUpdateTrackingTable::RemovalBehavior m_trackingRemovalBehavior = PackageUpdateTrackingTable::RemovalBehavior::Delete; // Set when the tables that this interface reads are the merged views over a delta and its - // baseline rather than tables of this database. Version 2.0 cannot produce that state - // itself; a derived version sets this when it establishes the views. + // baseline rather than tables of this database. mutable bool m_isDeltaReadMode = false; // Interface to the data before PrepareForPackaging is called. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp index 17c70f68b7..c3974e5b7b 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/Interface_2_0.cpp @@ -732,7 +732,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 auto idRowId = V1_0::IdTable::SelectIdByValue(connection, packageIdentifier); THROW_HR_IF(E_NOT_VALID_STATE, !idRowId); - SQLite::rowid_t packageId = PackagesTable::InsertWithRowId(connection, idRowId.value(), packageData); + SQLite::rowid_t packageId = PackagesTable::Insert(connection, packageData, idRowId); PackagesTable::UpdateValueIdById(connection, packageId, PackageUpdateTrackingTable::GetDataHash(connection, packageIdentifier, m_trackingRemovalBehavior)); @@ -749,7 +749,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } - // Extension point for later schema versions; see the declaration for why it must be here. CreateAdditionalPackagingOutput(context); PackagesTable::PrepareForPackaging< diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp index 8e630e1227..9539ad2dbb 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp @@ -7,12 +7,6 @@ #include #include -#include -#include -#include -#include -#include - using namespace AppInstaller::SQLite; namespace AppInstaller::Repository::Microsoft::Schema::V2_0 @@ -33,20 +27,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 namespace { // Finds the rowid that the package occupies in the index. - // - // The identity of a package is the rowid of its `ids` row, not the identifier string. - // AddManifest calls IdTable::EnsureExists with overwriteLikeMatch, so identifiers that - // match under LIKE collapse onto a single rowid and the stored string is replaced by the - // most recent casing. Matching by rowid therefore inherits the index's own notion of - // identity, and is stable against that string changing underneath us. std::optional GetPackageRowIdInIndex(const SQLite::Connection& connection, const std::string& packageIdentifier) { return V1_0::IdTable::SelectIdByValue(connection, packageIdentifier, true); } // The tombstones, as the identifier recorded for the package and the rowid it vacated. - // Generation needs only the rowid, so the public accessor reports that; the consistency - // check needs both in order to say which package is at fault. std::vector> GetRemovedRows( const SQLite::Connection& connection, PackageUpdateTrackingTable::RemovalBehavior removals) @@ -73,12 +59,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } // The sequence to stamp on the row about to be written. - // - // Allocated as one above the highest ever issued rather than from a stored counter, which - // needs no separate value to keep in step with the table and no migration when it is - // absent. It never goes backwards: rows are only ever inserted or updated in place while - // removals are being recorded, so no sequence is released once issued. SQLite answers the - // aggregate from the index with a single seek. int64_t GetNextChangeSequence(const SQLite::Connection& connection) { Builder::StatementBuilder builder; @@ -96,12 +76,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } // The rows written after the given point, as measured by the given column. - // - // The boundary is exclusive for the sequence and inclusive for the write time. That is not - // an inconsistency: whole second times cannot distinguish "written during the base second, - // before the base was taken" from "after", so the inclusive form is the only safe one and - // it over-reports by design. A sequence has no such ambiguity, so it takes the exact - // boundary and reports precisely what followed. + // The boundary is exclusive for the sequence and inclusive for the write time. std::vector GetUpdates( const SQLite::Connection& connection, std::string_view boundaryColumn, @@ -165,13 +140,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } // The rowids vacated after the given point, as measured by the given column. - // - // The rowid is reported rather than the identifier because it is the identity a delta is - // keyed on, and it is the only one that can be compared exactly. Identifiers cannot: the - // casing recorded for a package is frozen when its row is written, so one package can leave - // tombstones under several spellings, and no string comparison available here reproduces - // the ICU LIKE that decides identity elsewhere. Two packages can still vacate the same - // rowid in turn, so the result is a set. std::set GetRemovals( const SQLite::Connection& connection, std::string_view boundaryColumn, @@ -226,15 +194,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (removals == RemovalBehavior::Record) { builder.Column(ColumnBuilder(s_PUTT_IsRemoved, Type::Int64).NotNull().Default(0)); - - // The rowid the package occupies in the index, which is what a delta is keyed on. - // Always known: the add path resolves it while the package is present, and the remove - // path is given it by the caller, which resolves it before the package leaves. builder.Column(ColumnBuilder(s_PUTT_PackageRowId, Type::Int64).NotNull()); - - // A monotonically increasing stamp identifying when this row was last written, - // relative only to the other rows in this table. See GetUpdatesSinceSequence for why - // a delta uses this in preference to the write time. builder.Column(ColumnBuilder(s_PUTT_ChangeSequence, Type::Int64).NotNull()); } @@ -333,14 +293,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 else { // Mark the package as removed rather than deleting the row, clearing the data columns. - // The row is found by the rowid the package occupied rather than by its identifier: - // that is the identity a delta is keyed on, and matching on it means a package whose - // identifier changed casing still marks the row it actually owns. Earlier tombstones - // refer to rowids the package has already vacated and must be preserved so that a - // delta learns about each of them. - // - // The rowid cannot be looked up here, because the package has already left the index. - // The caller resolves it beforehand and passes it in. + // The row is found by the rowid the package occupied rather than by its identifier. THROW_HR_IF(E_NOT_VALID_STATE, !removedPackageRowId); int64_t currentTime = Utility::GetCurrentUnixEpoch(); @@ -515,10 +468,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } - // A package recorded as removed must no longer occupy the rowid it vacated. Comparing the - // rowid rather than merely checking for absence is what makes this precise: a package that - // was removed and re-added is legitimately back in the index, but at a different rowid, and - // the tombstone for the one it gave up is still meaningful. + // A package recorded as removed must no longer occupy the rowid it vacated. for (const auto& [packageIdentifier, vacatedRowId] : GetRemovedRows(connection, removals)) { std::optional indexRowId = GetPackageRowIdInIndex(connection, packageIdentifier); @@ -537,9 +487,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 } } - // Every live row must name the rowid that the package actually occupies, since that is the - // identity a delta is keyed on. A disagreement means either this table or the rowid - // pinning performed during packaging has drifted. + // Every live row must name the rowid that the package actually occupies. if (removals == RemovalBehavior::Record) { for (const PackageData& packageData : GetUpdatesSince(connection, 0, removals)) @@ -680,9 +628,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 packageRowIdBuilder.Execute(connection); // Every existing row is backfilled with the same sequence, and the next one issued is 1. - // That is correct rather than merely convenient: an index designated as a baseline - // immediately after migrating records 0 as its own sequence, so a delta against it reports - // only what followed - and what preceded is exactly what the baseline already contains. Builder::StatementBuilder changeSequenceBuilder; changeSequenceBuilder.AlterTable(s_PUTT_Table_Name).Add(Builder::ColumnBuilder(s_PUTT_ChangeSequence, Builder::Type::Int64).NotNull().Default(0)); changeSequenceBuilder.Execute(connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h index 9abc67195e..2cc518cb62 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.h @@ -4,8 +4,6 @@ #include "Microsoft/Schema/ISQLiteIndex.h" #include -#include - namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { @@ -14,10 +12,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 struct PackageUpdateTrackingTable { // Determines how the removal of a package is reflected in the table. - // Schema 2.0 deletes the row outright, so the table has no record that the package - // ever existed. Schema 2.1 instead marks the row as removed, which is what allows a - // delta index to express a removal; that requires the `is_removed` column, which only - // exists on tables created or migrated by 2.1. enum class RemovalBehavior { Delete, @@ -46,9 +40,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 static bool Exists(const SQLite::Connection& connection); // Updates the tracking table for the given package identifier in the internal index. - // When the package is no longer present and removals are being recorded, removedPackageRowId - // must carry the rowid it occupied; the caller has to resolve that before removing it from - // the index, as the ids row is gone by the time this is called. static void Update(SQLite::Connection& connection, const ISQLiteIndex* internalIndex, const std::string& packageIdentifier, RemovalBehavior removals, bool ensureTable = true, std::optional removedPackageRowId = {}); // Checks the consistency of the index to ensure that every referenced row exists. @@ -64,7 +55,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 SQLite::blob_t Manifest; SQLite::blob_t Hash; // The rowid the package occupies in the index, or 0 when it is not known. - // Only recorded when removals are being recorded; see the column comment. SQLite::rowid_t PackageRowId = 0; }; @@ -74,8 +64,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Gets the rowids vacated by packages removed since the given base time. // Only meaningful when removals are being recorded; always empty otherwise. - // The rowid is reported rather than the identifier because it is the identity a delta is - // keyed on and the only one that can be compared exactly; see the implementation. static std::set GetRemovalsSince(const SQLite::Connection& connection, int64_t updateBaseTime, RemovalBehavior removals); // Gets the data on updates written after the given change sequence, exclusive. @@ -93,9 +81,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // Gets the data hash for the given package identifier. static SQLite::blob_t GetDataHash(const SQLite::Connection& connection, const std::string& packageIdentifier, RemovalBehavior removals); - // Adds the columns needed to record removals to an existing table that does not have them, - // and backfills the package rowid for the rows already present. - // Used when migrating from schema 2.0 to 2.1; does nothing if the table does not exist. + // Adds the columns needed to record removals to an existing table that does not have them. static void AddRemovalTrackingColumns(SQLite::Connection& connection); }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.cpp index ad656b1ecc..a30c92c0e4 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.cpp @@ -213,44 +213,28 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 savepoint.Commit(); } - SQLite::rowid_t PackagesTable::Insert(SQLite::Connection& connection, const std::vector& values) + SQLite::rowid_t PackagesTable::Insert(SQLite::Connection& connection, const std::vector& values, std::optional rowid) { SQLite::Builder::StatementBuilder builder; builder.InsertInto(s_PackagesTable_Table_Name).BeginColumns(); - for (const NameValuePair& value : values) + if (rowid) { - builder.Column(value.Name); + builder.Column(SQLite::RowIDName); } - builder.EndColumns().BeginValues(); - for (const NameValuePair& value : values) { - builder.Value(value.Value); + builder.Column(value.Name); } - builder.EndValues(); - - builder.Execute(connection); - - return connection.GetLastInsertRowID(); - } - - SQLite::rowid_t PackagesTable::InsertWithRowId(SQLite::Connection& connection, SQLite::rowid_t rowid, const std::vector& values) - { - SQLite::Builder::StatementBuilder builder; - builder.InsertInto(s_PackagesTable_Table_Name).BeginColumns(); + builder.EndColumns().BeginValues(); - builder.Column(SQLite::RowIDName); - for (const NameValuePair& value : values) + if (rowid) { - builder.Column(value.Name); + builder.Value(rowid.value()); } - builder.EndColumns().BeginValues(); - - builder.Value(rowid); for (const NameValuePair& value : values) { builder.Value(value.Value); @@ -260,7 +244,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 builder.Execute(connection); - return rowid; + return rowid ? rowid.value() : connection.GetLastInsertRowID(); } bool PackagesTable::ExistsById(const SQLite::Connection& connection, SQLite::rowid_t id) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.h index 789111fc12..7f44f400b9 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackagesTable.h @@ -144,10 +144,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 }; // Insert the given values into the table. - static SQLite::rowid_t Insert(SQLite::Connection& connection, const std::vector& values); - - // Insert the given values into the table at a specific rowid. - static SQLite::rowid_t InsertWithRowId(SQLite::Connection& connection, SQLite::rowid_t rowid, const std::vector& values); + static SQLite::rowid_t Insert(SQLite::Connection& connection, const std::vector& values, std::optional rowid = std::nullopt); // Gets a value indicating whether the package with rowid exists. static bool ExistsById(const SQLite::Connection& connection, SQLite::rowid_t rowid); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp index 754d9633c3..1cdae2edd7 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaGeneration.cpp @@ -13,11 +13,6 @@ #include #include -#include -#include -#include -#include - namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta { @@ -25,10 +20,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta namespace { - // The delta database is created through SQLiteStorageBase rather than as a bare connection - // so that it carries the same metadata as any other index: a schema version, a database - // identifier, and a last write time. Without that metadata it could not be opened, as - // opening reads the schema version to decide which interface to use. struct DeltaDatabase : public SQLite::SQLiteStorageBase { DeltaDatabase(const std::filesystem::path& path, const SQLite::Version& version) : @@ -134,8 +125,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta } // Records that the package with the given identifier is no longer present. - // The rowid is the one that the baseline gave the package, as that is what the rest of the - // baseline data refers to. void WriteRemovedPackage(SQLite::Connection& deltaConnection, SQLite::rowid_t packageRowId, const std::string& packageIdentifier) { std::string tableName = GetTableName(V2_0::PackagesTable::TableName()); @@ -199,9 +188,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta builder.Execute(deltaConnection); } - // Records only the system reference values that changed for the package, rather than its - // entire set of values. A package with many product codes that gains one more therefore - // costs a single row. + // Records only the system reference values that changed for the package. void WriteSystemReferenceDifference( SQLite::Connection& deltaConnection, const SQLite::Connection& sourceConnection, @@ -226,9 +213,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta } // Gets a rowid that identifies the value in the merged data table, creating one if needed. - // Reusing the baseline's rowid where possible keeps the delta data table to just the values - // that the baseline has never seen; new rowids continue above the baseline's maximum so - // that the two tables can be combined without renumbering either of them. SQLite::rowid_t EnsureValueRowId( SQLite::Connection& deltaConnection, const SQLite::Connection& baselineConnection, @@ -282,8 +266,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta builder.Execute(deltaConnection); } - // Records only the map entries that changed for the package; see the note on the system - // reference equivalent for why the full set is not written. + // Records only the map entries that changed for the package. void WriteOneToManyDifference( SQLite::Connection& deltaConnection, const SQLite::Connection& sourceConnection, @@ -392,12 +375,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta if (writtenRowIds.count(removedRowId)) { - // The rowid has already been written, either by a package that has since taken it - // or by an earlier tombstone that vacated it. Writing it again is both impossible, - // since the rowid is the primary key of the delta's package table, and unnecessary: - // the row already there suppresses the baseline row, and where a new occupant wrote - // it the association differences were computed against the baseline at that same - // rowid, so the old package's data is displaced entirely. + // The rowid has already been written by a package that has since taken it.. AICLI_LOG(Repo, Verbose, << "Delta: rowid " << removedRowId << " was vacated but has already been written"); continue; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp index ffaa5a2b63..c35f97744d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaTables.cpp @@ -96,11 +96,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta ColumnBuilder(s_Delta_IsRemovedColumn, Type::Int64).NotNull() }); builder.Execute(connection); - - // No index on the identifier. Identity here is the rowid, and one identifier can - // legitimately occupy two rows: a package removed and re-added within the window - // vacates one rowid and takes another, which is recorded as a removal at the first and - // a change at the second. Uniqueness on the rowid is already given by the primary key. } // The system reference tables hold the value itself, so the delta only adds the removal flag. @@ -162,12 +157,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta using namespace SQLite::Builder; // Every index here exists only to serve generation: those on the one to many data tables - // let generation find the rowid it already allocated for a value. Nothing reads them. - // - // The merged views need no index at all. They suppress baseline packages by rowid, and - // baseline associations by the (value, package) pair that is the primary key of a WITHOUT - // ROWID table, so every probe already lands on a key. This matches the 2.0 index itself, - // which drops all of its indexes in PrepareForPackaging and ships as plain tables. + // let generation find the rowid it already allocated for a value. { SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "delta_preparetables_v2_1"); @@ -182,9 +172,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta savepoint.Commit(); } - // Generation only ever inserts, so there is nothing to reclaim from the data itself. The - // indexes just dropped are the exception, and the whole point of a delta is the bytes it - // costs to deliver, so it is worth returning those pages to the file. StatementBuilder vacuumBuilder; vacuumBuilder.Vacuum(); vacuumBuilder.Execute(connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp index 71fb64f7a8..1a60bacc6a 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.cpp @@ -12,9 +12,6 @@ #include #include -#include -#include - namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta { @@ -98,8 +95,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta // // The delta records only the associations that changed, rather than the full current set // for a changed package. A baseline association therefore survives unless the delta names - // that exact pair, or the package it belongs to has gone away entirely. Suppressing at the - // level of the package instead would discard every association a changed package still has. + // that exact pair, or the package it belongs to has gone away entirely. void CreateAssociationView( SQLite::Connection& connection, std::string_view viewName, @@ -151,13 +147,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta } // Verifies that the baseline is the one that the delta was generated against. - // - // Merging a delta with any other baseline produces plausible looking nonsense rather than - // an error: the packages it did not change are taken from a version of the world it never - // saw, and the rowids that tie the two together mean different things on each side. - // - // The baseline is read on a connection of its own because the metadata accessors always - // read the main database, and by the time it is attached it is not that. void ValidateBaselineAffinity(const SQLite::Connection& connection, const SQLite::DatabaseSpecifier& baseline) { std::optional expected = diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h index 015d6afa50..0bded48d1b 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/DeltaViews.h @@ -3,8 +3,6 @@ #pragma once #include -#include - namespace AppInstaller::Repository::Microsoft::Schema::V2_1::Delta { diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h index c69e21b408..5d66e4dfbc 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface.h @@ -5,19 +5,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 { - // The point in time from which the next delta generated against this index should be computed. - static constexpr std::string_view s_MetadataValueName_DeltaBaselineTime = "deltaBaselineTime"sv; - // The change sequence from which the next delta generated against this index should be computed. - // This is the value a delta actually uses; the time above is retained for the 2.0 export and for - // diagnostics. A sequence gives an exact, exclusive boundary that whole second times cannot. static constexpr std::string_view s_MetadataValueName_DeltaBaselineSequence = "deltaBaselineSequence"sv; // Identifies this index as a baseline that deltas may be generated against. - // - // The database identifier cannot serve this purpose. An index is prepared from a copy of a - // long lived working index, and copying carries the identifier along, so every index produced - // in a baseline period shares one. Designation therefore stamps its own fresh identity. static constexpr std::string_view s_MetadataValueName_BaselineIdentifier = "baselineIdentifier"sv; // Written into a delta, naming the baseline that it was generated against. A delta is only @@ -48,7 +39,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 void SetupDeltaReadMode(SQLite::Connection& connection, const SQLite::DatabaseSpecifier& baseline) override; protected: - // Records the baseline time for this index, and generates a delta index against a previous + // Records the baseline sequence for this index, and generates a delta index against a previous // baseline when the caller has supplied the paths to do so. void CreateAdditionalPackagingOutput(const SQLiteIndexContext& context) override; }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp index e4f9fb642b..71511e52a0 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_1/Interface_2_1.cpp @@ -9,14 +9,12 @@ #include #include -#include - namespace AppInstaller::Repository::Microsoft::Schema::V2_1 { Interface::Interface(Utility::NormalizationVersion normVersion) : V2_0::Interface(normVersion) { // Removals are recorded rather than deleted, so that delta generation can see which - // packages have gone away. This is the difference that 2.1 exists for. + // packages have gone away. m_trackingRemovalBehavior = V2_0::PackageUpdateTrackingTable::RemovalBehavior::Record; } @@ -36,7 +34,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 // Attempt a migration to 2.0 first, which will only return true if it actually performed a migration bool v2result = V2_0::Interface::MigrateFrom(connection, current); - // Migration from 2.0 → 2.1: add the is_removed column to update_tracking. + // Migration from 2.0 → 2.1 if (v2result || (currentVersion.MajorVersion == 2 && currentVersion.MinorVersion == 0)) { V2_0::PackageUpdateTrackingTable::AddRemovalTrackingColumns(connection); @@ -77,24 +75,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 { SQLite::Connection& connection = context.Connection; - // Record the point from which a delta against this index should be computed. Every 2.1 index - // does this, because any of them may later be designated as a baseline. - // TODO: We also need to ensure that our times are UTC / not impacted by timezone shifts, etc. - SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_DeltaBaselineTime, std::to_string(Utility::GetCurrentUnixEpoch())); - - // The sequence is what a delta actually uses; the time is retained because it is what the - // 2.0 version data manifest export reads, and because it remains useful diagnostically. - // A sequence is preferred here because the boundary it defines is exact. Whole second times - // cannot separate a change written during the baseline's own second from one written before - // it, so the time based window has to be inclusive and re-carries everything written in that - // second. A sequence is also immune to the clock stepping backwards, which under the time - // scheme silently drops a change and leaves a stale baseline row visible forever. - // - // It has to be recorded rather than recomputed from the baseline later: preparing an index - // drops the tracking table, so a baseline has none to read. That is also why this runs where - // it does, before the drop. Even had the table survived, its maximum is taken over whatever - // rows remain and would fall below the true high water mark once any were removed, so a - // later delta would re-carry changes the baseline already contains. int64_t currentSequence = V2_0::PackageUpdateTrackingTable::GetCurrentChangeSequence(connection, m_trackingRemovalBehavior); SQLite::MetadataTable::SetNamedValue(connection, s_MetadataValueName_DeltaBaselineSequence, std::to_string(currentSequence)); @@ -111,7 +91,13 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 SQLite::Connection baselineConnection = SQLite::Connection::Create(baselinePath.u8string(), SQLite::Connection::OpenDisposition::ReadOnly); - // The changes to capture are those written after the baseline recorded its own sequence. + // The baseline must be a previous version of this database. + std::string databaseIdentifier = SQLite::MetadataTable::GetNamedValue(connection, SQLite::s_MetadataValueName_DatabaseIdentifier); + std::string baselineDatabaseIdentifier = SQLite::MetadataTable::GetNamedValue(baselineConnection, SQLite::s_MetadataValueName_DatabaseIdentifier); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED, databaseIdentifier != baselineDatabaseIdentifier); + + // Ensure that the baseline is earlier in the sequence. int64_t baselineSequence = 0; std::optional baselineSequenceString = SQLite::MetadataTable::TryGetNamedValue(baselineConnection, s_MetadataValueName_DeltaBaselineSequence); if (baselineSequenceString && !baselineSequenceString->empty()) @@ -119,10 +105,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_1 baselineSequence = std::stoll(baselineSequenceString.value()); } - // A sequence fails in the one direction a time does not: if this index was rebuilt since the - // baseline was taken, its counter restarted below the baseline's value and the window is - // empty. That would produce a silently empty delta, so refuse instead. The equal case is - // legitimate and simply means nothing has changed. THROW_HR_IF(APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED, currentSequence < baselineSequence); auto changedPackages = V2_0::PackageUpdateTrackingTable::GetUpdatesSinceSequence(connection, baselineSequence, m_trackingRemovalBehavior); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp index 890317e8ce..efa1558155 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.cpp @@ -56,9 +56,8 @@ namespace AppInstaller::Repository::Microsoft::Schema return versionCreatorMap[std::min(static_cast(version.MinorVersion), versionCreatorMap.size() - 1)](); } - // Version 2.x is designed solely for minimizing the size of the index for transport. + // Version 2.* is designed solely for minimizing the size of the index for transport. // Unless it is prepared for packaging, it will be identical to a 1.N index. - // Version 2.1 adds is_removed tracking to enable delta index generation. if (version.MajorVersion == 2) { constexpr std::array(*)(), 2> versionCreatorMap = diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h index 9c2eebc92c..5680aed3c1 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h @@ -155,12 +155,12 @@ namespace AppInstaller::Repository::Microsoft::Schema // Version 2.1 // Designates this index as a baseline that delta indexes may be generated against, giving - // it an identity that a delta can name. A schema version that cannot be a baseline throws. + // it an identity that a delta can name. virtual void MarkAsBaseline(SQLite::Connection& connection); // Sets this index up to read the combination of a delta and the baseline that it was // generated against, so that every subsequent read sees the merged data. Must be called - // before any read. A schema version that cannot read a delta throws. + // before any read. virtual void SetupDeltaReadMode(SQLite::Connection& connection, const SQLite::DatabaseSpecifier& baseline); };