aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/zen/authutils.cpp2
-rw-r--r--src/zen/cmds/admin_cmd.h40
-rw-r--r--src/zen/cmds/bench_cmd.h5
-rw-r--r--src/zen/cmds/builds_cmd.cpp79
-rw-r--r--src/zen/cmds/cache_cmd.h20
-rw-r--r--src/zen/cmds/copy_cmd.h5
-rw-r--r--src/zen/cmds/dedup_cmd.h5
-rw-r--r--src/zen/cmds/exec_cmd.cpp9
-rw-r--r--src/zen/cmds/info_cmd.h5
-rw-r--r--src/zen/cmds/print_cmd.cpp4
-rw-r--r--src/zen/cmds/print_cmd.h10
-rw-r--r--src/zen/cmds/projectstore_cmd.cpp30
-rw-r--r--src/zen/cmds/projectstore_cmd.h60
-rw-r--r--src/zen/cmds/rpcreplay_cmd.h15
-rw-r--r--src/zen/cmds/run_cmd.h5
-rw-r--r--src/zen/cmds/serve_cmd.h5
-rw-r--r--src/zen/cmds/status_cmd.h5
-rw-r--r--src/zen/cmds/top_cmd.h10
-rw-r--r--src/zen/cmds/trace_cmd.h7
-rw-r--r--src/zen/cmds/ui_cmd.cpp236
-rw-r--r--src/zen/cmds/ui_cmd.h32
-rw-r--r--src/zen/cmds/up_cmd.h15
-rw-r--r--src/zen/cmds/vfs_cmd.h5
-rw-r--r--src/zen/cmds/wipe_cmd.cpp2
-rw-r--r--src/zen/cmds/workspaces_cmd.cpp2
-rw-r--r--src/zen/progressbar.cpp56
-rw-r--r--src/zen/progressbar.h1
-rw-r--r--src/zen/zen.cpp92
-rw-r--r--src/zencompute-test/xmake.lua1
-rw-r--r--src/zencompute/xmake.lua2
-rw-r--r--src/zencore/base64.cpp192
-rw-r--r--src/zencore/compactbinaryyaml.cpp427
-rw-r--r--src/zencore/filesystem.cpp20
-rw-r--r--src/zencore/include/zencore/base64.h4
-rw-r--r--src/zencore/include/zencore/compactbinaryvalue.h24
-rw-r--r--src/zencore/include/zencore/filesystem.h38
-rw-r--r--src/zencore/include/zencore/hashutils.h3
-rw-r--r--src/zencore/include/zencore/iobuffer.h37
-rw-r--r--src/zencore/include/zencore/process.h1
-rw-r--r--src/zencore/include/zencore/sharedbuffer.h13
-rw-r--r--src/zencore/intmath.cpp6
-rw-r--r--src/zencore/iobuffer.cpp20
-rw-r--r--src/zencore/memtrack/callstacktrace.cpp8
-rw-r--r--src/zencore/process.cpp226
-rw-r--r--src/zencore/string.cpp4
-rw-r--r--src/zencore/trace.cpp13
-rw-r--r--src/zencore/xmake.lua2
-rw-r--r--src/zenhttp/httpclient.cpp38
-rw-r--r--src/zenhttp/httpclientauth.cpp2
-rw-r--r--src/zenhttp/httpserver.cpp24
-rw-r--r--src/zenhttp/include/zenhttp/httpclient.h9
-rw-r--r--src/zenhttp/include/zenhttp/httpserver.h16
-rw-r--r--src/zenhttp/servers/httpasio.cpp52
-rw-r--r--src/zenhttp/servers/httpmulti.cpp10
-rw-r--r--src/zenhttp/servers/httpmulti.h13
-rw-r--r--src/zenhttp/servers/httpparser.cpp9
-rw-r--r--src/zenhttp/servers/httpsys.cpp66
-rw-r--r--src/zenremotestore/builds/buildstoragecache.cpp8
-rw-r--r--src/zenremotestore/builds/buildstorageoperations.cpp914
-rw-r--r--src/zenremotestore/builds/buildstorageutil.cpp19
-rw-r--r--src/zenremotestore/chunking/chunkblock.cpp581
-rw-r--r--src/zenremotestore/include/zenremotestore/builds/buildstoragecache.h1
-rw-r--r--src/zenremotestore/include/zenremotestore/builds/buildstorageoperations.h62
-rw-r--r--src/zenremotestore/include/zenremotestore/builds/buildstorageutil.h4
-rw-r--r--src/zenremotestore/include/zenremotestore/chunking/chunkblock.h99
-rw-r--r--src/zenremotestore/include/zenremotestore/jupiter/jupiterhost.h1
-rw-r--r--src/zenremotestore/include/zenremotestore/operationlogoutput.h5
-rw-r--r--src/zenremotestore/include/zenremotestore/partialblockrequestmode.h20
-rw-r--r--src/zenremotestore/include/zenremotestore/projectstore/buildsremoteprojectstore.h4
-rw-r--r--src/zenremotestore/include/zenremotestore/projectstore/remoteprojectstore.h67
-rw-r--r--src/zenremotestore/jupiter/jupiterhost.cpp8
-rw-r--r--src/zenremotestore/operationlogoutput.cpp2
-rw-r--r--src/zenremotestore/partialblockrequestmode.cpp27
-rw-r--r--src/zenremotestore/projectstore/buildsremoteprojectstore.cpp122
-rw-r--r--src/zenremotestore/projectstore/fileremoteprojectstore.cpp24
-rw-r--r--src/zenremotestore/projectstore/jupiterremoteprojectstore.cpp19
-rw-r--r--src/zenremotestore/projectstore/remoteprojectstore.cpp946
-rw-r--r--src/zenremotestore/projectstore/zenremoteprojectstore.cpp29
-rw-r--r--src/zenserver/compute/computeserver.cpp6
-rw-r--r--src/zenserver/frontend/frontend.cpp56
-rw-r--r--src/zenserver/frontend/frontend.h7
-rw-r--r--src/zenserver/frontend/html.zipbin238188 -> 279965 bytes
-rw-r--r--src/zenserver/frontend/zipfs.cpp20
-rw-r--r--src/zenserver/frontend/zipfs.h8
-rw-r--r--src/zenserver/hub/hubservice.cpp12
-rw-r--r--src/zenserver/hub/zenhubserver.cpp2
-rw-r--r--src/zenserver/main.cpp9
-rw-r--r--src/zenserver/storage/buildstore/httpbuildstore.cpp2
-rw-r--r--src/zenserver/storage/projectstore/httpprojectstore.cpp33
-rw-r--r--src/zenserver/storage/zenstorageserver.cpp9
-rw-r--r--src/zenserver/zenserver.cpp2
-rw-r--r--src/zenstore/buildstore/buildstore.cpp3
-rw-r--r--src/zenstore/cache/cachedisklayer.cpp4
-rw-r--r--src/zenstore/cache/cacherpc.cpp2
-rw-r--r--src/zenstore/cache/structuredcachestore.cpp5
-rw-r--r--src/zenstore/cas.cpp12
-rw-r--r--src/zenstore/gc.cpp7
-rw-r--r--src/zenstore/include/zenstore/gc.h2
-rw-r--r--src/zentest-appstub/xmake.lua2
-rw-r--r--src/zenutil/consoletui.cpp483
-rw-r--r--src/zenutil/include/zenutil/consoletui.h59
101 files changed, 4280 insertions, 1469 deletions
diff --git a/src/zen/authutils.cpp b/src/zen/authutils.cpp
index 31db82efd..16427acf5 100644
--- a/src/zen/authutils.cpp
+++ b/src/zen/authutils.cpp
@@ -233,7 +233,7 @@ AuthCommandLineOptions::ParseOptions(cxxopts::Options& Ops,
}
else if (!m_AccessTokenPath.empty())
{
- MakeSafeAbsolutePathÍnPlace(m_AccessTokenPath);
+ MakeSafeAbsolutePathInPlace(m_AccessTokenPath);
std::string ResolvedAccessToken = ReadAccessTokenFromJsonFile(m_AccessTokenPath);
if (!ResolvedAccessToken.empty())
{
diff --git a/src/zen/cmds/admin_cmd.h b/src/zen/cmds/admin_cmd.h
index 87ef8091b..83bcf8893 100644
--- a/src/zen/cmds/admin_cmd.h
+++ b/src/zen/cmds/admin_cmd.h
@@ -13,6 +13,9 @@ namespace zen {
class ScrubCommand : public StorageCommand
{
public:
+ static constexpr char Name[] = "scrub";
+ static constexpr char Description[] = "Scrub zen storage (verify data integrity)";
+
ScrubCommand();
~ScrubCommand();
@@ -20,7 +23,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"scrub", "Scrub zen storage"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
bool m_DryRun = false;
bool m_NoGc = false;
@@ -33,6 +36,9 @@ private:
class GcCommand : public StorageCommand
{
public:
+ static constexpr char Name[] = "gc";
+ static constexpr char Description[] = "Garbage collect zen storage";
+
GcCommand();
~GcCommand();
@@ -40,7 +46,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"gc", "Garbage collect zen storage"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
bool m_SmallObjects{false};
bool m_SkipCid{false};
@@ -62,6 +68,9 @@ private:
class GcStatusCommand : public StorageCommand
{
public:
+ static constexpr char Name[] = "gc-status";
+ static constexpr char Description[] = "Garbage collect zen storage status check";
+
GcStatusCommand();
~GcStatusCommand();
@@ -69,7 +78,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"gc-status", "Garbage collect zen storage status check"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
bool m_Details = false;
};
@@ -77,6 +86,9 @@ private:
class GcStopCommand : public StorageCommand
{
public:
+ static constexpr char Name[] = "gc-stop";
+ static constexpr char Description[] = "Request cancel of running garbage collection in zen storage";
+
GcStopCommand();
~GcStopCommand();
@@ -84,7 +96,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"gc-stop", "Request cancel of running garbage collection in zen storage"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
};
@@ -93,6 +105,9 @@ private:
class JobCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "jobs";
+ static constexpr char Description[] = "Show/cancel zen background jobs";
+
JobCommand();
~JobCommand();
@@ -100,7 +115,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"jobs", "Show/cancel zen background jobs"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::uint64_t m_JobId = 0;
bool m_Cancel = 0;
@@ -111,6 +126,9 @@ private:
class LoggingCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "logs";
+ static constexpr char Description[] = "Show/control zen logging";
+
LoggingCommand();
~LoggingCommand();
@@ -118,7 +136,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"logs", "Show/control zen logging"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_CacheWriteLog;
std::string m_CacheAccessLog;
@@ -133,6 +151,9 @@ private:
class FlushCommand : public StorageCommand
{
public:
+ static constexpr char Name[] = "flush";
+ static constexpr char Description[] = "Flush storage";
+
FlushCommand();
~FlushCommand();
@@ -140,7 +161,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"flush", "Flush zen storage"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
};
@@ -149,6 +170,9 @@ private:
class CopyStateCommand : public StorageCommand
{
public:
+ static constexpr char Name[] = "copy-state";
+ static constexpr char Description[] = "Copy zen server disk state";
+
CopyStateCommand();
~CopyStateCommand();
@@ -156,7 +180,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"copy-state", "Copy zen server disk state"};
+ cxxopts::Options m_Options{Name, Description};
std::filesystem::path m_DataPath;
std::filesystem::path m_TargetPath;
bool m_SkipLogs = false;
diff --git a/src/zen/cmds/bench_cmd.h b/src/zen/cmds/bench_cmd.h
index ed123be75..7fbf85340 100644
--- a/src/zen/cmds/bench_cmd.h
+++ b/src/zen/cmds/bench_cmd.h
@@ -9,6 +9,9 @@ namespace zen {
class BenchCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "bench";
+ static constexpr char Description[] = "Utility command for benchmarking";
+
BenchCommand();
~BenchCommand();
@@ -17,7 +20,7 @@ public:
virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; }
private:
- cxxopts::Options m_Options{"bench", "Benchmarking utility command"};
+ cxxopts::Options m_Options{Name, Description};
bool m_PurgeStandbyLists = false;
bool m_SingleProcess = false;
};
diff --git a/src/zen/cmds/builds_cmd.cpp b/src/zen/cmds/builds_cmd.cpp
index 59b209384..5254ef3cf 100644
--- a/src/zen/cmds/builds_cmd.cpp
+++ b/src/zen/cmds/builds_cmd.cpp
@@ -1467,9 +1467,16 @@ namespace {
ZEN_CONSOLE("Downloading build {}, parts:{} to '{}' ({})", BuildId, BuildPartString.ToView(), Path, NiceBytes(RawSize));
}
+ Stopwatch IndexTimer;
+
const ChunkedContentLookup LocalLookup = BuildChunkedContentLookup(LocalState.State.ChunkedContent);
const ChunkedContentLookup RemoteLookup = BuildChunkedContentLookup(RemoteContent);
+ if (!IsQuiet)
+ {
+ ZEN_OPERATION_LOG_INFO(Output, "Indexed local and remote content in {}", NiceTimeSpanMs(IndexTimer.GetElapsedTimeMs()));
+ }
+
ProgressBar::SetLogOperationProgress(ProgressMode, TaskSteps::Download, TaskSteps::StepCount);
BuildsOperationUpdateFolder Updater(
@@ -2680,7 +2687,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_SystemRootDir = PickDefaultSystemRootDirectory();
}
- MakeSafeAbsolutePathÍnPlace(m_SystemRootDir);
+ MakeSafeAbsolutePathInPlace(m_SystemRootDir);
};
ParseSystemOptions();
@@ -2729,7 +2736,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
throw OptionParseException("'--host', '--url', '--override-host' or '--storage-path' is required", SubOption->help());
}
- MakeSafeAbsolutePathÍnPlace(m_StoragePath);
+ MakeSafeAbsolutePathInPlace(m_StoragePath);
};
auto ParseOutputOptions = [&]() {
@@ -2835,13 +2842,16 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
TempPath / "storage");
Result.StorageName = ResolveRes.HostName;
- StorageDescription = fmt::format("Cloud {}{}. SessionId: '{}'. Namespace '{}', Bucket '{}'",
- ResolveRes.HostName,
- (ResolveRes.HostUrl == ResolveRes.HostName) ? "" : fmt::format(" {}", ResolveRes.HostUrl),
- Result.BuildStorageHttp->GetSessionId(),
- m_Namespace,
- m_Bucket);
- ;
+ uint64_t HostLatencyNs = ResolveRes.HostLatencySec >= 0 ? uint64_t(ResolveRes.HostLatencySec * 1000000000.0) : 0;
+
+ StorageDescription = fmt::format("Cloud {}{}. SessionId: '{}'. Namespace '{}', Bucket '{}'. Latency: {}",
+ ResolveRes.HostName,
+ (ResolveRes.HostUrl == ResolveRes.HostName) ? "" : fmt::format(" {}", ResolveRes.HostUrl),
+ Result.BuildStorageHttp->GetSessionId(),
+ m_Namespace,
+ m_Bucket,
+ NiceLatencyNs(HostLatencyNs));
+ Result.BuildStorageLatencySec = ResolveRes.HostLatencySec;
if (!ResolveRes.CacheUrl.empty())
{
@@ -2867,12 +2877,17 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
: GetTinyWorkerPool(EWorkloadType::Background));
Result.CacheName = ResolveRes.CacheName;
+ uint64_t CacheLatencyNs = ResolveRes.CacheLatencySec >= 0 ? uint64_t(ResolveRes.CacheLatencySec * 1000000000.0) : 0;
+
CacheDescription =
- fmt::format("Zen {}{}. SessionId: '{}'",
+ fmt::format("Zen {}{}. SessionId: '{}'. Latency: {}",
ResolveRes.CacheName,
(ResolveRes.CacheUrl == ResolveRes.CacheName) ? "" : fmt::format(" {}", ResolveRes.CacheUrl),
- Result.CacheHttp->GetSessionId());
- ;
+ Result.CacheHttp->GetSessionId(),
+ NiceLatencyNs(CacheLatencyNs));
+
+ Result.CacheLatencySec = ResolveRes.CacheLatencySec;
+
if (!m_Namespace.empty())
{
CacheDescription += fmt::format(". Namespace '{}'", m_Namespace);
@@ -2947,7 +2962,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
throw OptionParseException("'--local-path' is required", SubOption->help());
}
- MakeSafeAbsolutePathÍnPlace(m_Path);
+ MakeSafeAbsolutePathInPlace(m_Path);
};
auto ParseFileFilters = [&](std::vector<std::string>& OutIncludeWildcards, std::vector<std::string>& OutExcludeWildcards) {
@@ -3004,7 +3019,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
throw OptionParseException("'--compare-path' is required", SubOption->help());
}
- MakeSafeAbsolutePathÍnPlace(m_DiffPath);
+ MakeSafeAbsolutePathInPlace(m_DiffPath);
};
auto ParseBlobHash = [&]() -> IoHash {
@@ -3105,7 +3120,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
if (!m_BuildMetadataPath.empty())
{
- MakeSafeAbsolutePathÍnPlace(m_BuildMetadataPath);
+ MakeSafeAbsolutePathInPlace(m_BuildMetadataPath);
IoBuffer MetaDataJson = ReadFile(m_BuildMetadataPath).Flatten();
std::string_view Json(reinterpret_cast<const char*>(MetaDataJson.GetData()), MetaDataJson.GetSize());
std::string JsonError;
@@ -3202,8 +3217,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
if (SubOption == &m_ListOptions)
{
- MakeSafeAbsolutePathÍnPlace(m_ListQueryPath);
- MakeSafeAbsolutePathÍnPlace(m_ListResultPath);
+ MakeSafeAbsolutePathInPlace(m_ListQueryPath);
+ MakeSafeAbsolutePathInPlace(m_ListResultPath);
if (!m_ListResultPath.empty())
{
@@ -3255,7 +3270,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_ZenFolderPath = std::filesystem::current_path() / ZenFolderName;
}
- MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath);
+ MakeSafeAbsolutePathInPlace(m_ZenFolderPath);
CreateDirectories(m_ZenFolderPath);
auto _ = MakeGuard([this]() { CleanAndRemoveDirectory(GetSmallWorkerPool(EWorkloadType::Burst), m_ZenFolderPath); });
@@ -3294,7 +3309,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
if (SubOption == &m_ListBlocksOptions)
{
- MakeSafeAbsolutePathÍnPlace(m_ListResultPath);
+ MakeSafeAbsolutePathInPlace(m_ListResultPath);
if (!m_ListResultPath.empty())
{
@@ -3316,7 +3331,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_ZenFolderPath = std::filesystem::current_path() / ZenFolderName;
}
- MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath);
+ MakeSafeAbsolutePathInPlace(m_ZenFolderPath);
CreateDirectories(m_ZenFolderPath);
auto _ = MakeGuard([this]() { CleanAndRemoveDirectory(GetSmallWorkerPool(EWorkloadType::Burst), m_ZenFolderPath); });
@@ -3387,8 +3402,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_ZenFolderPath = std::filesystem::current_path() / ZenFolderName;
}
- MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath);
- MakeSafeAbsolutePathÍnPlace(m_ChunkingCachePath);
+ MakeSafeAbsolutePathInPlace(m_ZenFolderPath);
+ MakeSafeAbsolutePathInPlace(m_ChunkingCachePath);
CreateDirectories(m_ZenFolderPath);
auto _ = MakeGuard([this, &Workers]() { CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), m_ZenFolderPath); });
@@ -3532,7 +3547,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_ZenFolderPath = m_Path / ZenFolderName;
}
- MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath);
+ MakeSafeAbsolutePathInPlace(m_ZenFolderPath);
BuildStorageBase::Statistics StorageStats;
BuildStorageCache::Statistics StorageCacheStats;
@@ -3632,7 +3647,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_ZenFolderPath = m_Path / ZenFolderName;
}
- MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath);
+ MakeSafeAbsolutePathInPlace(m_ZenFolderPath);
BuildStorageBase::Statistics StorageStats;
BuildStorageCache::Statistics StorageCacheStats;
@@ -3652,7 +3667,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
std::unique_ptr<CbObjectWriter> StructuredOutput;
if (!m_LsResultPath.empty())
{
- MakeSafeAbsolutePathÍnPlace(m_LsResultPath);
+ MakeSafeAbsolutePathInPlace(m_LsResultPath);
StructuredOutput = std::make_unique<CbObjectWriter>();
}
@@ -3696,7 +3711,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
ParsePath();
ParseDiffPath();
- MakeSafeAbsolutePathÍnPlace(m_ChunkingCachePath);
+ MakeSafeAbsolutePathInPlace(m_ChunkingCachePath);
std::vector<std::string> ExcludeFolders = DefaultExcludeFolders;
std::vector<std::string> ExcludeExtensions = DefaultExcludeExtensions;
@@ -3745,7 +3760,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_ZenFolderPath = std::filesystem::current_path() / ZenFolderName;
}
- MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath);
+ MakeSafeAbsolutePathInPlace(m_ZenFolderPath);
CreateDirectories(m_ZenFolderPath);
auto _ = MakeGuard([this, &Workers]() { CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), m_ZenFolderPath); });
@@ -3828,7 +3843,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_ZenFolderPath = std::filesystem::current_path() / ZenFolderName;
}
- MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath);
+ MakeSafeAbsolutePathInPlace(m_ZenFolderPath);
CreateDirectories(m_ZenFolderPath);
auto _ = MakeGuard([this, &Workers]() { CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), m_ZenFolderPath); });
@@ -3883,7 +3898,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_ZenFolderPath = std::filesystem::current_path() / ZenFolderName;
}
- MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath);
+ MakeSafeAbsolutePathInPlace(m_ZenFolderPath);
CreateDirectories(m_ZenFolderPath);
auto _ = MakeGuard([this, &Workers]() { CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), m_ZenFolderPath); });
@@ -3933,7 +3948,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_ZenFolderPath = m_Path / ZenFolderName;
}
- MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath);
+ MakeSafeAbsolutePathInPlace(m_ZenFolderPath);
EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests();
@@ -4083,8 +4098,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
m_ZenFolderPath = m_Path / ZenFolderName;
}
- MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath);
- MakeSafeAbsolutePathÍnPlace(m_ChunkingCachePath);
+ MakeSafeAbsolutePathInPlace(m_ZenFolderPath);
+ MakeSafeAbsolutePathInPlace(m_ChunkingCachePath);
StorageInstance Storage = CreateBuildStorage(StorageStats,
StorageCacheStats,
diff --git a/src/zen/cmds/cache_cmd.h b/src/zen/cmds/cache_cmd.h
index 4dc05bbdc..4f5b90f4d 100644
--- a/src/zen/cmds/cache_cmd.h
+++ b/src/zen/cmds/cache_cmd.h
@@ -9,6 +9,9 @@ namespace zen {
class DropCommand : public CacheStoreCommand
{
public:
+ static constexpr char Name[] = "drop";
+ static constexpr char Description[] = "Drop cache namespace or bucket";
+
DropCommand();
~DropCommand();
@@ -16,7 +19,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"drop", "Drop cache namespace or bucket"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_NamespaceName;
std::string m_BucketName;
@@ -25,13 +28,16 @@ private:
class CacheInfoCommand : public CacheStoreCommand
{
public:
+ static constexpr char Name[] = "cache-info";
+ static constexpr char Description[] = "Info on cache, namespace or bucket";
+
CacheInfoCommand();
~CacheInfoCommand();
virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"cache-info", "Info on cache, namespace or bucket"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_NamespaceName;
std::string m_SizeInfoBucketNames;
@@ -42,26 +48,32 @@ private:
class CacheStatsCommand : public CacheStoreCommand
{
public:
+ static constexpr char Name[] = "cache-stats";
+ static constexpr char Description[] = "Stats on cache";
+
CacheStatsCommand();
~CacheStatsCommand();
virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"cache-stats", "Stats info on cache"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
};
class CacheDetailsCommand : public CacheStoreCommand
{
public:
+ static constexpr char Name[] = "cache-details";
+ static constexpr char Description[] = "Details on cache";
+
CacheDetailsCommand();
~CacheDetailsCommand();
virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"cache-details", "Detailed info on cache"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
bool m_CSV = false;
bool m_Details = false;
diff --git a/src/zen/cmds/copy_cmd.h b/src/zen/cmds/copy_cmd.h
index e1a5dcb82..757a8e691 100644
--- a/src/zen/cmds/copy_cmd.h
+++ b/src/zen/cmds/copy_cmd.h
@@ -11,6 +11,9 @@ namespace zen {
class CopyCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "copy";
+ static constexpr char Description[] = "Copy file(s)";
+
CopyCommand();
~CopyCommand();
@@ -19,7 +22,7 @@ public:
virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; }
private:
- cxxopts::Options m_Options{"copy", "Copy files efficiently"};
+ cxxopts::Options m_Options{Name, Description};
std::filesystem::path m_CopySource;
std::filesystem::path m_CopyTarget;
bool m_NoClone = false;
diff --git a/src/zen/cmds/dedup_cmd.h b/src/zen/cmds/dedup_cmd.h
index 5b8387dd2..835b35e92 100644
--- a/src/zen/cmds/dedup_cmd.h
+++ b/src/zen/cmds/dedup_cmd.h
@@ -11,6 +11,9 @@ namespace zen {
class DedupCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "dedup";
+ static constexpr char Description[] = "Dedup files";
+
DedupCommand();
~DedupCommand();
@@ -19,7 +22,7 @@ public:
virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; }
private:
- cxxopts::Options m_Options{"dedup", "Deduplicate files"};
+ cxxopts::Options m_Options{Name, Description};
std::vector<std::string> m_Positional;
std::filesystem::path m_DedupSource;
std::filesystem::path m_DedupTarget;
diff --git a/src/zen/cmds/exec_cmd.cpp b/src/zen/cmds/exec_cmd.cpp
index 2d9d0d12e..407f42ee3 100644
--- a/src/zen/cmds/exec_cmd.cpp
+++ b/src/zen/cmds/exec_cmd.cpp
@@ -360,6 +360,13 @@ ExecCommand::ExecUsingSession(zen::compute::FunctionServiceSession& FunctionSess
return false;
};
+ int TargetParallelism = 8;
+
+ if (OffsetCounter || StrideCounter || m_Limit)
+ {
+ TargetParallelism = 1;
+ }
+
m_RecordingReader->IterateActions(
[&](CbObject ActionObject, const IoHash& ActionId) {
// Enqueue job
@@ -444,7 +451,7 @@ ExecCommand::ExecUsingSession(zen::compute::FunctionServiceSession& FunctionSess
DrainCompletedJobs();
},
- 8);
+ TargetParallelism);
// Wait until all pending work is complete
diff --git a/src/zen/cmds/info_cmd.h b/src/zen/cmds/info_cmd.h
index 231565bfd..dc108b8a2 100644
--- a/src/zen/cmds/info_cmd.h
+++ b/src/zen/cmds/info_cmd.h
@@ -9,6 +9,9 @@ namespace zen {
class InfoCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "info";
+ static constexpr char Description[] = "Show high level Zen server information";
+
InfoCommand();
~InfoCommand();
@@ -17,7 +20,7 @@ public:
// virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; }
private:
- cxxopts::Options m_Options{"info", "Show high level zen store information"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
};
diff --git a/src/zen/cmds/print_cmd.cpp b/src/zen/cmds/print_cmd.cpp
index 030cc8b66..c6b250fdf 100644
--- a/src/zen/cmds/print_cmd.cpp
+++ b/src/zen/cmds/print_cmd.cpp
@@ -84,7 +84,7 @@ PrintCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
}
else
{
- MakeSafeAbsolutePathÍnPlace(m_Filename);
+ MakeSafeAbsolutePathInPlace(m_Filename);
Fc = ReadFile(m_Filename);
}
@@ -244,7 +244,7 @@ PrintPackageCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** ar
if (m_Filename.empty())
throw OptionParseException("'--source' is required", m_Options.help());
- MakeSafeAbsolutePathÍnPlace(m_Filename);
+ MakeSafeAbsolutePathInPlace(m_Filename);
FileContents Fc = ReadFile(m_Filename);
IoBuffer Data = Fc.Flatten();
CbPackage Package;
diff --git a/src/zen/cmds/print_cmd.h b/src/zen/cmds/print_cmd.h
index 6c1529b7c..f4a97e218 100644
--- a/src/zen/cmds/print_cmd.h
+++ b/src/zen/cmds/print_cmd.h
@@ -11,6 +11,9 @@ namespace zen {
class PrintCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "print";
+ static constexpr char Description[] = "Print compact binary object";
+
PrintCommand();
~PrintCommand();
@@ -19,7 +22,7 @@ public:
virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; }
private:
- cxxopts::Options m_Options{"print", "Print compact binary object"};
+ cxxopts::Options m_Options{Name, Description};
std::filesystem::path m_Filename;
bool m_ShowCbObjectTypeInfo = false;
};
@@ -29,6 +32,9 @@ private:
class PrintPackageCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "printpackage";
+ static constexpr char Description[] = "Print compact binary package";
+
PrintPackageCommand();
~PrintPackageCommand();
@@ -37,7 +43,7 @@ public:
virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; }
private:
- cxxopts::Options m_Options{"printpkg", "Print compact binary package"};
+ cxxopts::Options m_Options{Name, Description};
std::filesystem::path m_Filename;
bool m_ShowCbObjectTypeInfo = false;
};
diff --git a/src/zen/cmds/projectstore_cmd.cpp b/src/zen/cmds/projectstore_cmd.cpp
index 4885fd363..bedab3cfd 100644
--- a/src/zen/cmds/projectstore_cmd.cpp
+++ b/src/zen/cmds/projectstore_cmd.cpp
@@ -1469,6 +1469,20 @@ ImportOplogCommand::ImportOplogCommand()
"Enables both 'boost-worker-count' and 'boost-worker-memory' - may cause computer to be less responsive",
cxxopts::value(m_BoostWorkers),
"<boostworkermemory>");
+ m_Options.add_option(
+ "",
+ "",
+ "allow-partial-block-requests",
+ "Allow request for partial chunk blocks.\n"
+ " false = only full block requests allowed\n"
+ " mixed = multiple partial block ranges requests per block allowed to zen cache, single partial block range "
+ "request per block to host\n"
+ " zencacheonly = multiple partial block ranges requests per block allowed to zen cache, only full block requests "
+ "allowed to host\n"
+ " true = multiple partial block ranges requests per block allowed to zen cache and host\n"
+ "Defaults to 'mixed'.",
+ cxxopts::value(m_AllowPartialBlockRequests),
+ "<allowpartialblockrequests>");
m_Options.parse_positional({"project", "oplog", "gcpath"});
m_Options.positional_help("[<projectid> <oplogid> [<gcpath>]]");
@@ -1513,6 +1527,13 @@ ImportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg
throw OptionParseException("'--oplog' is required", m_Options.help());
}
+ EPartialBlockRequestMode Mode = PartialBlockRequestModeFromString(m_AllowPartialBlockRequests);
+ if (Mode == EPartialBlockRequestMode::Invalid)
+ {
+ throw OptionParseException(fmt::format("'--allow-partial-block-requests' ('{}') is invalid", m_AllowPartialBlockRequests),
+ m_Options.help());
+ }
+
HttpClient Http(m_HostName);
m_ProjectName = ResolveProject(Http, m_ProjectName);
if (m_ProjectName.empty())
@@ -1649,6 +1670,9 @@ ImportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg
{
Writer.AddBool("boostworkermemory"sv, true);
}
+
+ Writer.AddString("partialblockrequestmode", m_AllowPartialBlockRequests);
+
if (!m_FileDirectoryPath.empty())
{
Writer.BeginObject("file"sv);
@@ -2430,7 +2454,7 @@ OplogDownloadCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a
{
m_SystemRootDir = PickDefaultSystemRootDirectory();
}
- MakeSafeAbsolutePathÍnPlace(m_SystemRootDir);
+ MakeSafeAbsolutePathInPlace(m_SystemRootDir);
};
ParseSystemOptions();
@@ -2571,6 +2595,7 @@ OplogDownloadCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a
ClientSettings.AssumeHttp2 = ResolveRes.HostAssumeHttp2;
ClientSettings.MaximumInMemoryDownloadSize = m_BoostWorkerMemory ? RemoteStoreOptions::DefaultMaxBlockSize : 1024u * 1024u;
Storage.BuildStorageHttp = std::make_unique<HttpClient>(ResolveRes.HostUrl, ClientSettings);
+ Storage.BuildStorageLatencySec = ResolveRes.HostLatencySec;
BuildStorageCache::Statistics StorageCacheStats;
@@ -2589,7 +2614,8 @@ OplogDownloadCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a
.RetryCount = 0,
.MaximumInMemoryDownloadSize = m_BoostWorkerMemory ? RemoteStoreOptions::DefaultMaxBlockSize : 1024u * 1024u},
[&AbortFlag]() { return AbortFlag.load(); });
- Storage.CacheName = ResolveRes.CacheName;
+ Storage.CacheName = ResolveRes.CacheName;
+ Storage.CacheLatencySec = ResolveRes.CacheLatencySec;
}
if (!m_Quiet)
diff --git a/src/zen/cmds/projectstore_cmd.h b/src/zen/cmds/projectstore_cmd.h
index 56ef858f5..17fd76e9f 100644
--- a/src/zen/cmds/projectstore_cmd.h
+++ b/src/zen/cmds/projectstore_cmd.h
@@ -16,6 +16,9 @@ class ProjectStoreCommand : public ZenCmdBase
class DropProjectCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "project-drop";
+ static constexpr char Description[] = "Drop project or project oplog";
+
DropProjectCommand();
~DropProjectCommand();
@@ -23,7 +26,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"project-drop", "Drop project or project oplog"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_ProjectName;
std::string m_OplogName;
@@ -33,13 +36,16 @@ private:
class ProjectInfoCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "project-info";
+ static constexpr char Description[] = "Info on project or project oplog";
+
ProjectInfoCommand();
~ProjectInfoCommand();
virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"project-info", "Info on project or project oplog"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_ProjectName;
std::string m_OplogName;
@@ -48,6 +54,9 @@ private:
class CreateProjectCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "project-create";
+ static constexpr char Description[] = "Create a project";
+
CreateProjectCommand();
~CreateProjectCommand();
@@ -55,7 +64,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"project-create", "Create project, the project must not already exist."};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_ProjectId;
std::string m_RootDir;
@@ -68,6 +77,9 @@ private:
class CreateOplogCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "oplog-create";
+ static constexpr char Description[] = "Create a project oplog";
+
CreateOplogCommand();
~CreateOplogCommand();
@@ -75,7 +87,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"oplog-create", "Create oplog in an existing project, the oplog must not already exist."};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_ProjectId;
std::string m_OplogId;
@@ -86,6 +98,9 @@ private:
class ExportOplogCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "oplog-export";
+ static constexpr char Description[] = "Export project store oplog";
+
ExportOplogCommand();
~ExportOplogCommand();
@@ -93,8 +108,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"oplog-export",
- "Export project store oplog to cloud (--cloud), file system (--file) or other Zen instance (--zen)"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_ProjectName;
std::string m_OplogName;
@@ -145,6 +159,9 @@ private:
class ImportOplogCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "oplog-import";
+ static constexpr char Description[] = "Import project store oplog";
+
ImportOplogCommand();
~ImportOplogCommand();
@@ -152,8 +169,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"oplog-import",
- "Import project store oplog from cloud (--cloud), file system (--file) or other Zen instance (--zen)"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_ProjectName;
std::string m_OplogName;
@@ -193,19 +209,23 @@ private:
bool m_BoostWorkerCount = false;
bool m_BoostWorkerMemory = false;
bool m_BoostWorkers = false;
+
+ std::string m_AllowPartialBlockRequests = "mixed";
};
class SnapshotOplogCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "oplog-snapshot";
+ static constexpr char Description[] = "Snapshot project store oplog";
+
SnapshotOplogCommand();
~SnapshotOplogCommand();
-
virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"oplog-snapshot", "Snapshot external file references in project store oplog into zen"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_ProjectName;
std::string m_OplogName;
@@ -214,26 +234,32 @@ private:
class ProjectStatsCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "project-stats";
+ static constexpr char Description[] = "Stats on project store";
+
ProjectStatsCommand();
~ProjectStatsCommand();
virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"project-stats", "Stats info on project store"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
};
class ProjectOpDetailsCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "project-op-details";
+ static constexpr char Description[] = "Detail info on ops inside a project store oplog";
+
ProjectOpDetailsCommand();
~ProjectOpDetailsCommand();
virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"project-op-details", "Detail info on ops inside a project store oplog"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
bool m_Details = false;
bool m_OpDetails = false;
@@ -247,13 +273,16 @@ private:
class OplogMirrorCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "oplog-mirror";
+ static constexpr char Description[] = "Mirror project store oplog to file system";
+
OplogMirrorCommand();
~OplogMirrorCommand();
virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"oplog-mirror", "Mirror oplog to file system"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_ProjectName;
std::string m_OplogName;
@@ -268,13 +297,16 @@ private:
class OplogValidateCommand : public ProjectStoreCommand
{
public:
+ static constexpr char Name[] = "oplog-validate";
+ static constexpr char Description[] = "Validate oplog for missing references";
+
OplogValidateCommand();
~OplogValidateCommand();
virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"oplog-validate", "Validate oplog for missing references"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_ProjectName;
std::string m_OplogName;
diff --git a/src/zen/cmds/rpcreplay_cmd.h b/src/zen/cmds/rpcreplay_cmd.h
index a6363b614..332a3126c 100644
--- a/src/zen/cmds/rpcreplay_cmd.h
+++ b/src/zen/cmds/rpcreplay_cmd.h
@@ -9,6 +9,9 @@ namespace zen {
class RpcStartRecordingCommand : public CacheStoreCommand
{
public:
+ static constexpr char Name[] = "rpc-record-start";
+ static constexpr char Description[] = "Starts recording of cache rpc requests on a host";
+
RpcStartRecordingCommand();
~RpcStartRecordingCommand();
@@ -16,7 +19,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"rpc-record-start", "Starts recording of cache rpc requests on a host"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_RecordingPath;
};
@@ -24,6 +27,9 @@ private:
class RpcStopRecordingCommand : public CacheStoreCommand
{
public:
+ static constexpr char Name[] = "rpc-record-stop";
+ static constexpr char Description[] = "Stops recording of cache rpc requests on a host";
+
RpcStopRecordingCommand();
~RpcStopRecordingCommand();
@@ -31,13 +37,16 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"rpc-record-stop", "Stops recording of cache rpc requests on a host"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
};
class RpcReplayCommand : public CacheStoreCommand
{
public:
+ static constexpr char Name[] = "rpc-record-replay";
+ static constexpr char Description[] = "Replays a previously recorded session of rpc requests";
+
RpcReplayCommand();
~RpcReplayCommand();
@@ -45,7 +54,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"rpc-record-replay", "Replays a previously recorded session of cache rpc requests to a target host"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_RecordingPath;
bool m_OnHost = false;
diff --git a/src/zen/cmds/run_cmd.h b/src/zen/cmds/run_cmd.h
index 570a2e63a..300c08c5b 100644
--- a/src/zen/cmds/run_cmd.h
+++ b/src/zen/cmds/run_cmd.h
@@ -9,6 +9,9 @@ namespace zen {
class RunCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "run";
+ static constexpr char Description[] = "Run command with special options";
+
RunCommand();
~RunCommand();
@@ -17,7 +20,7 @@ public:
virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; }
private:
- cxxopts::Options m_Options{"run", "Run executable"};
+ cxxopts::Options m_Options{Name, Description};
int m_RunCount = 0;
int m_RunTime = -1;
std::string m_BaseDirectory;
diff --git a/src/zen/cmds/serve_cmd.h b/src/zen/cmds/serve_cmd.h
index ac74981f2..22f430948 100644
--- a/src/zen/cmds/serve_cmd.h
+++ b/src/zen/cmds/serve_cmd.h
@@ -11,6 +11,9 @@ namespace zen {
class ServeCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "serve";
+ static constexpr char Description[] = "Serve files from a directory";
+
ServeCommand();
~ServeCommand();
@@ -18,7 +21,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"serve", "Serve files from a tree"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
std::string m_ProjectName;
std::string m_OplogName;
diff --git a/src/zen/cmds/status_cmd.h b/src/zen/cmds/status_cmd.h
index dc103a196..df5df3066 100644
--- a/src/zen/cmds/status_cmd.h
+++ b/src/zen/cmds/status_cmd.h
@@ -11,6 +11,9 @@ namespace zen {
class StatusCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "status";
+ static constexpr char Description[] = "Show zen status";
+
StatusCommand();
~StatusCommand();
@@ -20,7 +23,7 @@ public:
private:
int GetLockFileEffectivePort() const;
- cxxopts::Options m_Options{"status", "Show zen status"};
+ cxxopts::Options m_Options{Name, Description};
uint16_t m_Port = 0;
std::filesystem::path m_DataDir;
};
diff --git a/src/zen/cmds/top_cmd.h b/src/zen/cmds/top_cmd.h
index 74167ecfd..aeb196558 100644
--- a/src/zen/cmds/top_cmd.h
+++ b/src/zen/cmds/top_cmd.h
@@ -9,6 +9,9 @@ namespace zen {
class TopCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "top";
+ static constexpr char Description[] = "Monitor zen server activity";
+
TopCommand();
~TopCommand();
@@ -16,12 +19,15 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"top", "Show dev UI"};
+ cxxopts::Options m_Options{Name, Description};
};
class PsCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "ps";
+ static constexpr char Description[] = "Enumerate running zen server instances";
+
PsCommand();
~PsCommand();
@@ -29,7 +35,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"ps", "Enumerate running Zen server instances"};
+ cxxopts::Options m_Options{Name, Description};
};
} // namespace zen
diff --git a/src/zen/cmds/trace_cmd.h b/src/zen/cmds/trace_cmd.h
index a6c9742b7..6eb0ba22b 100644
--- a/src/zen/cmds/trace_cmd.h
+++ b/src/zen/cmds/trace_cmd.h
@@ -6,11 +6,12 @@
namespace zen {
-/** Scrub storage
- */
class TraceCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "trace";
+ static constexpr char Description[] = "Control zen realtime tracing";
+
TraceCommand();
~TraceCommand();
@@ -18,7 +19,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"trace", "Control zen realtime tracing"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_HostName;
bool m_Stop = false;
std::string m_TraceHost;
diff --git a/src/zen/cmds/ui_cmd.cpp b/src/zen/cmds/ui_cmd.cpp
new file mode 100644
index 000000000..da06ce305
--- /dev/null
+++ b/src/zen/cmds/ui_cmd.cpp
@@ -0,0 +1,236 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "ui_cmd.h"
+
+#include <zencore/except_fmt.h>
+#include <zencore/fmtutils.h>
+#include <zencore/logging.h>
+#include <zencore/process.h>
+#include <zenutil/consoletui.h>
+#include <zenutil/zenserverprocess.h>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+# include <shellapi.h>
+#endif
+
+namespace zen {
+
+namespace {
+
+ struct RunningServerInfo
+ {
+ uint16_t Port;
+ uint32_t Pid;
+ std::string SessionId;
+ std::string CmdLine;
+ };
+
+ static std::vector<RunningServerInfo> CollectRunningServers()
+ {
+ std::vector<RunningServerInfo> Servers;
+ ZenServerState State;
+ if (!State.InitializeReadOnly())
+ return Servers;
+
+ State.Snapshot([&](const ZenServerState::ZenServerEntry& Entry) {
+ StringBuilder<25> SessionSB;
+ Entry.GetSessionId().ToString(SessionSB);
+ std::error_code CmdLineEc;
+ std::string CmdLine = GetProcessCommandLine(static_cast<int>(Entry.Pid.load()), CmdLineEc);
+ Servers.push_back({Entry.EffectiveListenPort.load(), Entry.Pid.load(), std::string(SessionSB.c_str()), std::move(CmdLine)});
+ });
+
+ return Servers;
+ }
+
+} // namespace
+
+UiCommand::UiCommand()
+{
+ m_Options.add_options()("h,help", "Print help");
+ m_Options.add_options()("a,all", "Open dashboard for all running instances", cxxopts::value(m_All)->default_value("false"));
+ m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), "<hosturl>");
+ m_Options.add_option("",
+ "p",
+ "path",
+ "Dashboard path (default: /dashboard/)",
+ cxxopts::value(m_DashboardPath)->default_value("/dashboard/"),
+ "<path>");
+ m_Options.parse_positional("path");
+}
+
+UiCommand::~UiCommand()
+{
+}
+
+void
+UiCommand::OpenBrowser(std::string_view HostName)
+{
+ // Allow shortcuts for specifying dashboard path, and ensure it is in a format we expect
+ // (leading slash, trailing slash if no file extension)
+
+ if (!m_DashboardPath.empty())
+ {
+ if (m_DashboardPath[0] != '/')
+ {
+ m_DashboardPath = "/dashboard/" + m_DashboardPath;
+ }
+
+ if (m_DashboardPath.find_last_of('.') == std::string::npos && m_DashboardPath.back() != '/')
+ {
+ m_DashboardPath += '/';
+ }
+ }
+
+ bool Success = false;
+
+ ExtendableStringBuilder<256> FullUrl;
+ FullUrl << HostName << m_DashboardPath;
+
+#if ZEN_PLATFORM_WINDOWS
+ HINSTANCE Result = ShellExecuteA(nullptr, "open", FullUrl.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ Success = reinterpret_cast<intptr_t>(Result) > 32;
+#else
+ // Validate URL doesn't contain shell metacharacters that could lead to command injection
+ std::string_view FullUrlView = FullUrl;
+ constexpr std::string_view DangerousChars = ";|&$`\\\"'<>(){}[]!#*?~\n\r";
+ if (FullUrlView.find_first_of(DangerousChars) != std::string_view::npos)
+ {
+ throw OptionParseException(fmt::format("URL contains invalid characters: '{}'", FullUrl), m_Options.help());
+ }
+
+# if ZEN_PLATFORM_MAC
+ std::string Command = fmt::format("open \"{}\"", FullUrl);
+# elif ZEN_PLATFORM_LINUX
+ std::string Command = fmt::format("xdg-open \"{}\"", FullUrl);
+# else
+ ZEN_NOT_IMPLEMENTED("Browser launching not implemented on this platform");
+# endif
+
+ Success = system(Command.c_str()) == 0;
+#endif
+
+ if (!Success)
+ {
+ throw zen::runtime_error("Failed to launch browser for '{}'", FullUrl);
+ }
+
+ ZEN_CONSOLE("Web browser launched for '{}' successfully", FullUrl);
+}
+
+void
+UiCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
+{
+ using namespace std::literals;
+
+ ZEN_UNUSED(GlobalOptions);
+
+ if (!ParseOptions(argc, argv))
+ {
+ return;
+ }
+
+ // Resolve target server
+ uint16_t ServerPort = 0;
+
+ if (m_HostName.empty())
+ {
+ // Auto-discover running instances.
+ std::vector<RunningServerInfo> Servers = CollectRunningServers();
+
+ if (m_All)
+ {
+ if (Servers.empty())
+ {
+ throw OptionParseException("No running Zen server instances found", m_Options.help());
+ }
+
+ for (const auto& Server : Servers)
+ {
+ OpenBrowser(fmt::format("http://localhost:{}", Server.Port));
+ }
+ return;
+ }
+
+ // If multiple are found and we have an interactive terminal, present a picker
+ // instead of silently using the first one.
+ if (Servers.size() > 1 && IsTuiAvailable())
+ {
+ std::vector<std::string> Labels;
+ Labels.reserve(Servers.size() + 1);
+ Labels.push_back(fmt::format("(all {} instances)", Servers.size()));
+
+ const int32_t Cols = static_cast<int32_t>(TuiConsoleColumns());
+ constexpr int32_t kIndicator = 3; // " ▶ " or " " prefix
+ constexpr int32_t kSeparator = 2; // " " before cmdline
+ constexpr int32_t kEllipsis = 3; // "..."
+
+ for (const auto& Server : Servers)
+ {
+ std::string Label = fmt::format("port {:<5} pid {:<7} session {}", Server.Port, Server.Pid, Server.SessionId);
+
+ if (!Server.CmdLine.empty())
+ {
+ int32_t Available = Cols - kIndicator - kSeparator - static_cast<int32_t>(Label.size());
+ if (Available > kEllipsis)
+ {
+ Label += " ";
+ if (static_cast<int32_t>(Server.CmdLine.size()) <= Available)
+ {
+ Label += Server.CmdLine;
+ }
+ else
+ {
+ Label.append(Server.CmdLine, 0, static_cast<size_t>(Available - kEllipsis));
+ Label += "...";
+ }
+ }
+ }
+
+ Labels.push_back(std::move(Label));
+ }
+
+ int SelectedIdx = TuiPickOne("Multiple Zen server instances found. Select one to open:", Labels);
+ if (SelectedIdx < 0)
+ return; // User cancelled
+
+ if (SelectedIdx == 0)
+ {
+ // "All" selected
+ for (const auto& Server : Servers)
+ {
+ OpenBrowser(fmt::format("http://localhost:{}", Server.Port));
+ }
+ return;
+ }
+
+ ServerPort = Servers[SelectedIdx - 1].Port;
+ m_HostName = fmt::format("http://localhost:{}", ServerPort);
+ }
+
+ if (m_HostName.empty())
+ {
+ // Single or zero instances, or not an interactive terminal:
+ // fall back to default resolution (picks first instance or returns empty)
+ m_HostName = ResolveTargetHostSpec("", ServerPort);
+ }
+ }
+ else
+ {
+ if (m_All)
+ {
+ throw OptionParseException("--all cannot be used together with --hosturl", m_Options.help());
+ }
+ m_HostName = ResolveTargetHostSpec(m_HostName, ServerPort);
+ }
+
+ if (m_HostName.empty())
+ {
+ throw OptionParseException("Unable to resolve server specification", m_Options.help());
+ }
+
+ OpenBrowser(m_HostName);
+}
+
+} // namespace zen
diff --git a/src/zen/cmds/ui_cmd.h b/src/zen/cmds/ui_cmd.h
new file mode 100644
index 000000000..c74cdbbd0
--- /dev/null
+++ b/src/zen/cmds/ui_cmd.h
@@ -0,0 +1,32 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "../zen.h"
+
+#include <filesystem>
+
+namespace zen {
+
+class UiCommand : public ZenCmdBase
+{
+public:
+ UiCommand();
+ ~UiCommand();
+
+ static constexpr char Name[] = "ui";
+ static constexpr char Description[] = "Launch web browser with zen server UI";
+
+ virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
+ virtual cxxopts::Options& Options() override { return m_Options; }
+
+private:
+ void OpenBrowser(std::string_view HostName);
+
+ cxxopts::Options m_Options{Name, Description};
+ std::string m_HostName;
+ std::string m_DashboardPath = "/dashboard/";
+ bool m_All = false;
+};
+
+} // namespace zen
diff --git a/src/zen/cmds/up_cmd.h b/src/zen/cmds/up_cmd.h
index 2e822d5fc..270db7f88 100644
--- a/src/zen/cmds/up_cmd.h
+++ b/src/zen/cmds/up_cmd.h
@@ -11,6 +11,9 @@ namespace zen {
class UpCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "up";
+ static constexpr char Description[] = "Bring zen server up";
+
UpCommand();
~UpCommand();
@@ -18,7 +21,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"up", "Bring up zen service"};
+ cxxopts::Options m_Options{Name, Description};
uint16_t m_Port = 0;
bool m_ShowConsole = false;
bool m_ShowLog = false;
@@ -28,6 +31,9 @@ private:
class AttachCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "attach";
+ static constexpr char Description[] = "Add a sponsor process to a running zen service";
+
AttachCommand();
~AttachCommand();
@@ -35,7 +41,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"attach", "Add a sponsor process to a running zen service"};
+ cxxopts::Options m_Options{Name, Description};
uint16_t m_Port = 0;
int m_OwnerPid = 0;
std::filesystem::path m_DataDir;
@@ -44,6 +50,9 @@ private:
class DownCommand : public ZenCmdBase
{
public:
+ static constexpr char Name[] = "down";
+ static constexpr char Description[] = "Bring zen server down";
+
DownCommand();
~DownCommand();
@@ -51,7 +60,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"down", "Bring down zen service"};
+ cxxopts::Options m_Options{Name, Description};
uint16_t m_Port = 0;
bool m_ForceTerminate = false;
std::filesystem::path m_ProgramBaseDir;
diff --git a/src/zen/cmds/vfs_cmd.h b/src/zen/cmds/vfs_cmd.h
index 5deaa02fa..9009c774b 100644
--- a/src/zen/cmds/vfs_cmd.h
+++ b/src/zen/cmds/vfs_cmd.h
@@ -9,6 +9,9 @@ namespace zen {
class VfsCommand : public StorageCommand
{
public:
+ static constexpr char Name[] = "vfs";
+ static constexpr char Description[] = "Manage virtual file system";
+
VfsCommand();
~VfsCommand();
@@ -16,7 +19,7 @@ public:
virtual cxxopts::Options& Options() override { return m_Options; }
private:
- cxxopts::Options m_Options{"vfs", "Manage virtual file system"};
+ cxxopts::Options m_Options{Name, Description};
std::string m_Verb;
std::string m_HostName;
diff --git a/src/zen/cmds/wipe_cmd.cpp b/src/zen/cmds/wipe_cmd.cpp
index adf0e61f0..a5029e1c5 100644
--- a/src/zen/cmds/wipe_cmd.cpp
+++ b/src/zen/cmds/wipe_cmd.cpp
@@ -549,7 +549,7 @@ WipeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
ProgressMode = (IsVerbose || m_PlainProgress) ? ProgressBar::Mode::Plain : ProgressBar::Mode::Pretty;
BoostWorkerThreads = m_BoostWorkerThreads;
- MakeSafeAbsolutePathÍnPlace(m_Directory);
+ MakeSafeAbsolutePathInPlace(m_Directory);
if (!IsDir(m_Directory))
{
diff --git a/src/zen/cmds/workspaces_cmd.cpp b/src/zen/cmds/workspaces_cmd.cpp
index 6e6f5d863..2661ac9da 100644
--- a/src/zen/cmds/workspaces_cmd.cpp
+++ b/src/zen/cmds/workspaces_cmd.cpp
@@ -398,7 +398,7 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char**
}
else
{
- MakeSafeAbsolutePathÍnPlace(m_SystemRootDir);
+ MakeSafeAbsolutePathInPlace(m_SystemRootDir);
}
std::filesystem::path StatePath = m_SystemRootDir / "workspaces";
diff --git a/src/zen/progressbar.cpp b/src/zen/progressbar.cpp
index 83606df67..732f16e81 100644
--- a/src/zen/progressbar.cpp
+++ b/src/zen/progressbar.cpp
@@ -8,16 +8,12 @@
#include <zencore/logging.h>
#include <zencore/windows.h>
#include <zenremotestore/operationlogoutput.h>
+#include <zenutil/consoletui.h>
ZEN_THIRD_PARTY_INCLUDES_START
#include <gsl/gsl-lite.hpp>
ZEN_THIRD_PARTY_INCLUDES_END
-#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
-# include <sys/ioctl.h>
-# include <unistd.h>
-#endif
-
//////////////////////////////////////////////////////////////////////////
namespace zen {
@@ -31,35 +27,12 @@ GetConsoleHandle()
}
#endif
-static bool
-CheckStdoutTty()
-{
-#if ZEN_PLATFORM_WINDOWS
- HANDLE hStdOut = GetConsoleHandle();
- DWORD dwMode = 0;
- static bool IsConsole = ::GetConsoleMode(hStdOut, &dwMode);
- return IsConsole;
-#else
- return isatty(fileno(stdout));
-#endif
-}
-
-static bool
-IsStdoutTty()
-{
- static bool StdoutIsTty = CheckStdoutTty();
- return StdoutIsTty;
-}
-
static void
OutputToConsoleRaw(const char* String, size_t Length)
{
#if ZEN_PLATFORM_WINDOWS
HANDLE hStdOut = GetConsoleHandle();
-#endif
-
-#if ZEN_PLATFORM_WINDOWS
- if (IsStdoutTty())
+ if (TuiIsStdoutTty())
{
WriteConsoleA(hStdOut, String, (DWORD)Length, 0, 0);
}
@@ -85,26 +58,6 @@ OutputToConsoleRaw(const StringBuilderBase& SB)
}
uint32_t
-GetConsoleColumns(uint32_t Default)
-{
-#if ZEN_PLATFORM_WINDOWS
- HANDLE hStdOut = GetConsoleHandle();
- CONSOLE_SCREEN_BUFFER_INFO csbi;
- if (GetConsoleScreenBufferInfo(hStdOut, &csbi) == TRUE)
- {
- return (uint32_t)(csbi.srWindow.Right - csbi.srWindow.Left + 1);
- }
-#else
- struct winsize w;
- if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0)
- {
- return (uint32_t)w.ws_col;
- }
-#endif
- return Default;
-}
-
-uint32_t
GetUpdateDelayMS(ProgressBar::Mode InMode)
{
switch (InMode)
@@ -165,7 +118,7 @@ ProgressBar::PopLogOperation(Mode InMode)
}
ProgressBar::ProgressBar(Mode InMode, std::string_view InSubTask)
-: m_Mode((!IsStdoutTty() && InMode == Mode::Pretty) ? Mode::Plain : InMode)
+: m_Mode((!TuiIsStdoutTty() && InMode == Mode::Pretty) ? Mode::Plain : InMode)
, m_LastUpdateMS((uint64_t)-1)
, m_PausedMS(0)
, m_SubTask(InSubTask)
@@ -245,6 +198,7 @@ ProgressBar::UpdateState(const State& NewState, bool DoLinebreak)
const std::string Details = (!NewState.Details.empty()) ? fmt::format(": {}", NewState.Details) : "";
const std::string Output = fmt::format("{} {}% ({}){}\n", Task, PercentDone, NiceTimeSpanMs(ElapsedTimeMS), Details);
OutputToConsoleRaw(Output);
+ m_State = NewState;
}
else if (m_Mode == Mode::Pretty)
{
@@ -256,7 +210,7 @@ ProgressBar::UpdateState(const State& NewState, bool DoLinebreak)
uint64_t ETAMS =
(NewState.Status == State::EStatus::Running) && (PercentDone > 5) ? (ETAElapsedMS * NewState.RemainingCount) / Completed : 0;
- uint32_t ConsoleColumns = GetConsoleColumns(1024);
+ uint32_t ConsoleColumns = TuiConsoleColumns(1024);
const std::string PercentString = fmt::format("{:#3}%", PercentDone);
diff --git a/src/zen/progressbar.h b/src/zen/progressbar.h
index bbdb008d4..cb1c7023b 100644
--- a/src/zen/progressbar.h
+++ b/src/zen/progressbar.h
@@ -76,7 +76,6 @@ private:
};
uint32_t GetUpdateDelayMS(ProgressBar::Mode InMode);
-uint32_t GetConsoleColumns(uint32_t Default);
OperationLogOutput* CreateConsoleLogOutput(ProgressBar::Mode InMode);
diff --git a/src/zen/zen.cpp b/src/zen/zen.cpp
index 018f77738..dc37cb56b 100644
--- a/src/zen/zen.cpp
+++ b/src/zen/zen.cpp
@@ -22,6 +22,7 @@
#include "cmds/status_cmd.h"
#include "cmds/top_cmd.h"
#include "cmds/trace_cmd.h"
+#include "cmds/ui_cmd.h"
#include "cmds/up_cmd.h"
#include "cmds/version_cmd.h"
#include "cmds/vfs_cmd.h"
@@ -41,6 +42,7 @@
#include <zencore/windows.h>
#include <zenhttp/httpcommon.h>
#include <zenutil/config/environmentoptions.h>
+#include <zenutil/consoletui.h>
#include <zenutil/logging.h>
#include <zenutil/workerpools.h>
#include <zenutil/zenserverprocess.h>
@@ -123,7 +125,7 @@ ZenCmdBase::ParseOptions(int argc, char** argv)
bool
ZenCmdBase::ParseOptions(cxxopts::Options& CmdOptions, int argc, char** argv)
{
- CmdOptions.set_width(GetConsoleColumns(80));
+ CmdOptions.set_width(TuiConsoleColumns(80));
cxxopts::ParseResult Result;
@@ -364,6 +366,7 @@ main(int argc, char** argv)
LoggingCommand LoggingCmd;
TopCommand TopCmd;
TraceCommand TraceCmd;
+ UiCommand UiCmd;
UpCommand UpCmd;
VersionCommand VersionCmd;
VfsCommand VfsCmd;
@@ -379,56 +382,57 @@ main(int argc, char** argv)
const char* CmdSummary;
} Commands[] = {
// clang-format off
- {"attach", &AttachCmd, "Add a sponsor process to a running zen service"},
- {"bench", &BenchCmd, "Utility command for benchmarking"},
- {BuildsCommand::Name, &BuildsCmd, BuildsCommand::Description},
- {"cache-details", &CacheDetailsCmd, "Details on cache"},
- {"cache-info", &CacheInfoCmd, "Info on cache, namespace or bucket"},
+ {AttachCommand::Name, &AttachCmd, AttachCommand::Description},
+ {BenchCommand::Name, &BenchCmd, BenchCommand::Description},
+ {BuildsCommand::Name, &BuildsCmd, BuildsCommand::Description},
+ {CacheDetailsCommand::Name, &CacheDetailsCmd, CacheDetailsCommand::Description},
+ {CacheInfoCommand::Name, &CacheInfoCmd, CacheInfoCommand::Description},
{CacheGetCommand::Name, &CacheGetCmd, CacheGetCommand::Description},
{CacheGenerateCommand::Name, &CacheGenerateCmd, CacheGenerateCommand::Description},
- {"cache-stats", &CacheStatsCmd, "Stats on cache"},
- {"copy", &CopyCmd, "Copy file(s)"},
- {"copy-state", &CopyStateCmd, "Copy zen server disk state"},
- {"dedup", &DedupCmd, "Dedup files"},
- {"down", &DownCmd, "Bring zen server down"},
- {"drop", &DropCmd, "Drop cache namespace or bucket"},
+ {CacheStatsCommand::Name, &CacheStatsCmd, CacheStatsCommand::Description},
+ {CopyCommand::Name, &CopyCmd, CopyCommand::Description},
+ {CopyStateCommand::Name, &CopyStateCmd, CopyStateCommand::Description},
+ {DedupCommand::Name, &DedupCmd, DedupCommand::Description},
+ {DownCommand::Name, &DownCmd, DownCommand::Description},
+ {DropCommand::Name, &DropCmd, DropCommand::Description},
#if ZEN_WITH_COMPUTE_SERVICES
{ExecCommand::Name, &ExecCmd, ExecCommand::Description},
#endif
- {"gc-status", &GcStatusCmd, "Garbage collect zen storage status check"},
- {"gc-stop", &GcStopCmd, "Request cancel of running garbage collection in zen storage"},
- {"gc", &GcCmd, "Garbage collect zen storage"},
- {"info", &InfoCmd, "Show high level Zen server information"},
- {"jobs", &JobCmd, "Show/cancel zen background jobs"},
- {"logs", &LoggingCmd, "Show/control zen logging"},
- {"oplog-create", &CreateOplogCmd, "Create a project oplog"},
- {"oplog-export", &ExportOplogCmd, "Export project store oplog"},
- {"oplog-import", &ImportOplogCmd, "Import project store oplog"},
- {"oplog-mirror", &OplogMirrorCmd, "Mirror project store oplog to file system"},
- {"oplog-snapshot", &SnapshotOplogCmd, "Snapshot project store oplog"},
+ {GcStatusCommand::Name, &GcStatusCmd, GcStatusCommand::Description},
+ {GcStopCommand::Name, &GcStopCmd, GcStopCommand::Description},
+ {GcCommand::Name, &GcCmd, GcCommand::Description},
+ {InfoCommand::Name, &InfoCmd, InfoCommand::Description},
+ {JobCommand::Name, &JobCmd, JobCommand::Description},
+ {LoggingCommand::Name, &LoggingCmd, LoggingCommand::Description},
+ {CreateOplogCommand::Name, &CreateOplogCmd, CreateOplogCommand::Description},
+ {ExportOplogCommand::Name, &ExportOplogCmd, ExportOplogCommand::Description},
+ {ImportOplogCommand::Name, &ImportOplogCmd, ImportOplogCommand::Description},
+ {OplogMirrorCommand::Name, &OplogMirrorCmd, OplogMirrorCommand::Description},
+ {SnapshotOplogCommand::Name, &SnapshotOplogCmd, SnapshotOplogCommand::Description},
{OplogDownloadCommand::Name, &OplogDownload, OplogDownloadCommand::Description},
- {"oplog-validate", &OplogValidateCmd, "Validate oplog for missing references"},
- {"print", &PrintCmd, "Print compact binary object"},
- {"printpackage", &PrintPkgCmd, "Print compact binary package"},
- {"project-create", &CreateProjectCmd, "Create a project"},
- {"project-op-details", &ProjectOpDetailsCmd, "Detail info on ops inside a project store oplog"},
- {"project-drop", &ProjectDropCmd, "Drop project or project oplog"},
- {"project-info", &ProjectInfoCmd, "Info on project or project oplog"},
- {"project-stats", &ProjectStatsCmd, "Stats on project store"},
- {"ps", &PsCmd, "Enumerate running zen server instances"},
- {"rpc-record-replay", &RpcReplayCmd, "Replays a previously recorded session of rpc requests"},
- {"rpc-record-start", &RpcStartRecordingCmd, "Starts recording of cache rpc requests on a host"},
- {"rpc-record-stop", &RpcStopRecordingCmd, "Stops recording of cache rpc requests on a host"},
- {"run", &RunCmd, "Run command with special options"},
- {"scrub", &ScrubCmd, "Scrub zen storage (verify data integrity)"},
- {"serve", &ServeCmd, "Serve files from a directory"},
- {"status", &StatusCmd, "Show zen status"},
- {"top", &TopCmd, "Monitor zen server activity"},
- {"trace", &TraceCmd, "Control zen realtime tracing"},
- {"up", &UpCmd, "Bring zen server up"},
+ {OplogValidateCommand::Name, &OplogValidateCmd, OplogValidateCommand::Description},
+ {PrintCommand::Name, &PrintCmd, PrintCommand::Description},
+ {PrintPackageCommand::Name, &PrintPkgCmd, PrintPackageCommand::Description},
+ {CreateProjectCommand::Name, &CreateProjectCmd, CreateProjectCommand::Description},
+ {ProjectOpDetailsCommand::Name, &ProjectOpDetailsCmd, ProjectOpDetailsCommand::Description},
+ {DropProjectCommand::Name, &ProjectDropCmd, DropProjectCommand::Description},
+ {ProjectInfoCommand::Name, &ProjectInfoCmd, ProjectInfoCommand::Description},
+ {ProjectStatsCommand::Name, &ProjectStatsCmd, ProjectStatsCommand::Description},
+ {PsCommand::Name, &PsCmd, PsCommand::Description},
+ {RpcReplayCommand::Name, &RpcReplayCmd, RpcReplayCommand::Description},
+ {RpcStartRecordingCommand::Name, &RpcStartRecordingCmd, RpcStartRecordingCommand::Description},
+ {RpcStopRecordingCommand::Name, &RpcStopRecordingCmd, RpcStopRecordingCommand::Description},
+ {RunCommand::Name, &RunCmd, RunCommand::Description},
+ {ScrubCommand::Name, &ScrubCmd, ScrubCommand::Description},
+ {ServeCommand::Name, &ServeCmd, ServeCommand::Description},
+ {StatusCommand::Name, &StatusCmd, StatusCommand::Description},
+ {TopCommand::Name, &TopCmd, TopCommand::Description},
+ {TraceCommand::Name, &TraceCmd, TraceCommand::Description},
+ {UiCommand::Name, &UiCmd, UiCommand::Description},
+ {UpCommand::Name, &UpCmd, UpCommand::Description},
{VersionCommand::Name, &VersionCmd, VersionCommand::Description},
- {"vfs", &VfsCmd, "Manage virtual file system"},
- {"flush", &FlushCmd, "Flush storage"},
+ {VfsCommand::Name, &VfsCmd, VfsCommand::Description},
+ {FlushCommand::Name, &FlushCmd, FlushCommand::Description},
{WipeCommand::Name, &WipeCmd, WipeCommand::Description},
{WorkspaceCommand::Name, &WorkspaceCmd, WorkspaceCommand::Description},
{WorkspaceShareCommand::Name, &WorkspaceShareCmd, WorkspaceShareCommand::Description},
diff --git a/src/zencompute-test/xmake.lua b/src/zencompute-test/xmake.lua
index 64a3c7703..1207bdefd 100644
--- a/src/zencompute-test/xmake.lua
+++ b/src/zencompute-test/xmake.lua
@@ -6,4 +6,3 @@ target("zencompute-test")
add_headerfiles("**.h")
add_files("*.cpp")
add_deps("zencompute", "zencore")
- add_packages("vcpkg::doctest")
diff --git a/src/zencompute/xmake.lua b/src/zencompute/xmake.lua
index c710b662d..50877508c 100644
--- a/src/zencompute/xmake.lua
+++ b/src/zencompute/xmake.lua
@@ -7,5 +7,3 @@ target('zencompute')
add_files("**.cpp")
add_includedirs("include", {public=true})
add_deps("zencore", "zenstore", "zenutil", "zennet", "zenhttp")
- add_packages("vcpkg::gsl-lite")
- add_packages("vcpkg::spdlog", "vcpkg::cxxopts")
diff --git a/src/zencore/base64.cpp b/src/zencore/base64.cpp
index 1f56ee6c3..fdf5f2d66 100644
--- a/src/zencore/base64.cpp
+++ b/src/zencore/base64.cpp
@@ -1,6 +1,10 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include <zencore/base64.h>
+#include <zencore/string.h>
+#include <zencore/testing.h>
+
+#include <string>
namespace zen {
@@ -11,7 +15,6 @@ static const uint8_t EncodingAlphabet[64] = {'A', 'B', 'C', 'D', 'E', 'F', 'G',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
/** The table used to convert an ascii character into a 6 bit value */
-#if 0
static const uint8_t DecodingAlphabet[256] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00-0x0f
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x10-0x1f
@@ -30,7 +33,6 @@ static const uint8_t DecodingAlphabet[256] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xe0-0xef
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 0xf0-0xff
};
-#endif // 0
template<typename CharType>
uint32_t
@@ -104,4 +106,190 @@ Base64::Encode(const uint8_t* Source, uint32_t Length, CharType* Dest)
template uint32_t Base64::Encode<char>(const uint8_t* Source, uint32_t Length, char* Dest);
template uint32_t Base64::Encode<wchar_t>(const uint8_t* Source, uint32_t Length, wchar_t* Dest);
+template<typename CharType>
+bool
+Base64::Decode(const CharType* Source, uint32_t Length, uint8_t* Dest, uint32_t& OutLength)
+{
+ // Length must be a multiple of 4
+ if (Length % 4 != 0)
+ {
+ OutLength = 0;
+ return false;
+ }
+
+ uint8_t* DecodedBytes = Dest;
+
+ // Process 4 encoded characters at a time, producing 3 decoded bytes
+ while (Length > 0)
+ {
+ // Count padding characters at the end
+ uint32_t PadCount = 0;
+ if (Source[3] == '=')
+ {
+ PadCount++;
+ if (Source[2] == '=')
+ {
+ PadCount++;
+ }
+ }
+
+ // Look up each character in the decoding table
+ uint8_t A = DecodingAlphabet[static_cast<uint8_t>(Source[0])];
+ uint8_t B = DecodingAlphabet[static_cast<uint8_t>(Source[1])];
+ uint8_t C = (PadCount >= 2) ? 0 : DecodingAlphabet[static_cast<uint8_t>(Source[2])];
+ uint8_t D = (PadCount >= 1) ? 0 : DecodingAlphabet[static_cast<uint8_t>(Source[3])];
+
+ // Check for invalid characters (0xFF means not in the base64 alphabet)
+ if (A == 0xFF || B == 0xFF || C == 0xFF || D == 0xFF)
+ {
+ OutLength = 0;
+ return false;
+ }
+
+ // Reconstruct the 24-bit value from 4 6-bit chunks
+ uint32_t ByteTriplet = (A << 18) | (B << 12) | (C << 6) | D;
+
+ // Extract the 3 bytes
+ *DecodedBytes++ = static_cast<uint8_t>(ByteTriplet >> 16);
+ if (PadCount < 2)
+ {
+ *DecodedBytes++ = static_cast<uint8_t>((ByteTriplet >> 8) & 0xFF);
+ }
+ if (PadCount < 1)
+ {
+ *DecodedBytes++ = static_cast<uint8_t>(ByteTriplet & 0xFF);
+ }
+
+ Source += 4;
+ Length -= 4;
+ }
+
+ OutLength = uint32_t(DecodedBytes - Dest);
+ return true;
+}
+
+template bool Base64::Decode<char>(const char* Source, uint32_t Length, uint8_t* Dest, uint32_t& OutLength);
+template bool Base64::Decode<wchar_t>(const wchar_t* Source, uint32_t Length, uint8_t* Dest, uint32_t& OutLength);
+
+//////////////////////////////////////////////////////////////////////////
+//
+// Testing related code follows...
+//
+
+#if ZEN_WITH_TESTS
+
+using namespace std::string_literals;
+
+TEST_CASE("Base64")
+{
+ auto EncodeString = [](std::string_view Input) -> std::string {
+ std::string Result;
+ Result.resize(Base64::GetEncodedDataSize(uint32_t(Input.size())));
+ Base64::Encode(reinterpret_cast<const uint8_t*>(Input.data()), uint32_t(Input.size()), Result.data());
+ return Result;
+ };
+
+ auto DecodeString = [](std::string_view Input) -> std::string {
+ std::string Result;
+ Result.resize(Base64::GetMaxDecodedDataSize(uint32_t(Input.size())));
+ uint32_t DecodedLength = 0;
+ bool Success = Base64::Decode(Input.data(), uint32_t(Input.size()), reinterpret_cast<uint8_t*>(Result.data()), DecodedLength);
+ CHECK(Success);
+ Result.resize(DecodedLength);
+ return Result;
+ };
+
+ SUBCASE("Encode")
+ {
+ CHECK(EncodeString("") == ""s);
+ CHECK(EncodeString("f") == "Zg=="s);
+ CHECK(EncodeString("fo") == "Zm8="s);
+ CHECK(EncodeString("foo") == "Zm9v"s);
+ CHECK(EncodeString("foob") == "Zm9vYg=="s);
+ CHECK(EncodeString("fooba") == "Zm9vYmE="s);
+ CHECK(EncodeString("foobar") == "Zm9vYmFy"s);
+ }
+
+ SUBCASE("Decode")
+ {
+ CHECK(DecodeString("") == ""s);
+ CHECK(DecodeString("Zg==") == "f"s);
+ CHECK(DecodeString("Zm8=") == "fo"s);
+ CHECK(DecodeString("Zm9v") == "foo"s);
+ CHECK(DecodeString("Zm9vYg==") == "foob"s);
+ CHECK(DecodeString("Zm9vYmE=") == "fooba"s);
+ CHECK(DecodeString("Zm9vYmFy") == "foobar"s);
+ }
+
+ SUBCASE("RoundTrip")
+ {
+ auto RoundTrip = [&](const std::string& Input) {
+ std::string Encoded = EncodeString(Input);
+ std::string Decoded = DecodeString(Encoded);
+ CHECK(Decoded == Input);
+ };
+
+ RoundTrip("Hello, World!");
+ RoundTrip("Base64 encoding test with various lengths");
+ RoundTrip("A");
+ RoundTrip("AB");
+ RoundTrip("ABC");
+ RoundTrip("ABCD");
+ RoundTrip("\x00\x01\x02\xff\xfe\xfd"s);
+ }
+
+ SUBCASE("BinaryRoundTrip")
+ {
+ // Test with all byte values 0-255
+ uint8_t AllBytes[256];
+ for (int i = 0; i < 256; ++i)
+ {
+ AllBytes[i] = static_cast<uint8_t>(i);
+ }
+
+ char Encoded[Base64::GetEncodedDataSize(256) + 1];
+ Base64::Encode(AllBytes, 256, Encoded);
+
+ uint8_t Decoded[256];
+ uint32_t DecodedLength = 0;
+ bool Success = Base64::Decode(Encoded, uint32_t(strlen(Encoded)), Decoded, DecodedLength);
+ CHECK(Success);
+ CHECK(DecodedLength == 256);
+ CHECK(memcmp(AllBytes, Decoded, 256) == 0);
+ }
+
+ SUBCASE("DecodeInvalidInput")
+ {
+ uint8_t Dest[64];
+ uint32_t OutLength = 0;
+
+ // Length not a multiple of 4
+ CHECK_FALSE(Base64::Decode("abc", 3u, Dest, OutLength));
+
+ // Invalid character
+ CHECK_FALSE(Base64::Decode("ab!d", 4u, Dest, OutLength));
+ }
+
+ SUBCASE("EncodedDataSize")
+ {
+ CHECK(Base64::GetEncodedDataSize(0) == 0);
+ CHECK(Base64::GetEncodedDataSize(1) == 4);
+ CHECK(Base64::GetEncodedDataSize(2) == 4);
+ CHECK(Base64::GetEncodedDataSize(3) == 4);
+ CHECK(Base64::GetEncodedDataSize(4) == 8);
+ CHECK(Base64::GetEncodedDataSize(5) == 8);
+ CHECK(Base64::GetEncodedDataSize(6) == 8);
+ }
+
+ SUBCASE("MaxDecodedDataSize")
+ {
+ CHECK(Base64::GetMaxDecodedDataSize(0) == 0);
+ CHECK(Base64::GetMaxDecodedDataSize(4) == 3);
+ CHECK(Base64::GetMaxDecodedDataSize(8) == 6);
+ CHECK(Base64::GetMaxDecodedDataSize(12) == 9);
+ }
+}
+
+#endif
+
} // namespace zen
diff --git a/src/zencore/compactbinaryyaml.cpp b/src/zencore/compactbinaryyaml.cpp
index 5122e952a..b308af418 100644
--- a/src/zencore/compactbinaryyaml.cpp
+++ b/src/zencore/compactbinaryyaml.cpp
@@ -14,11 +14,6 @@
#include <string_view>
#include <vector>
-ZEN_THIRD_PARTY_INCLUDES_START
-#include <ryml.hpp>
-#include <ryml_std.hpp>
-ZEN_THIRD_PARTY_INCLUDES_END
-
namespace zen {
//////////////////////////////////////////////////////////////////////////
@@ -26,193 +21,349 @@ namespace zen {
class CbYamlWriter
{
public:
- explicit CbYamlWriter(StringBuilderBase& InBuilder) : m_StrBuilder(InBuilder) { m_NodeStack.push_back(m_Tree.rootref()); }
+ explicit CbYamlWriter(StringBuilderBase& InBuilder) : m_Builder(InBuilder) {}
void WriteField(CbFieldView Field)
{
- ryml::NodeRef Node;
+ CbValue Accessor = Field.GetValue();
+ CbFieldType Type = Accessor.GetType();
- if (m_IsFirst)
+ switch (Type)
{
- Node = Top();
+ case CbFieldType::Object:
+ case CbFieldType::UniformObject:
+ WriteMapEntries(Field, 0);
+ break;
+ case CbFieldType::Array:
+ case CbFieldType::UniformArray:
+ WriteSeqEntries(Field, 0);
+ break;
+ default:
+ WriteScalarValue(Field);
+ m_Builder << '\n';
+ break;
+ }
+ }
+
+ void WriteMapEntry(CbFieldView Field, int32_t Indent)
+ {
+ WriteIndent(Indent);
+ WriteMapEntryContent(Field, Indent);
+ }
+
+ void WriteSeqEntry(CbFieldView Field, int32_t Indent)
+ {
+ CbValue Accessor = Field.GetValue();
+ CbFieldType Type = Accessor.GetType();
- m_IsFirst = false;
+ if (Type == CbFieldType::Object || Type == CbFieldType::UniformObject)
+ {
+ bool First = true;
+ for (CbFieldView MapChild : Field)
+ {
+ if (First)
+ {
+ WriteIndent(Indent);
+ m_Builder << "- ";
+ First = false;
+ }
+ else
+ {
+ WriteIndent(Indent + 1);
+ }
+ WriteMapEntryContent(MapChild, Indent + 1);
+ }
+ }
+ else if (Type == CbFieldType::Array || Type == CbFieldType::UniformArray)
+ {
+ WriteIndent(Indent);
+ m_Builder << "-\n";
+ WriteSeqEntries(Field, Indent + 1);
}
else
{
- Node = Top().append_child();
+ WriteIndent(Indent);
+ m_Builder << "- ";
+ WriteScalarValue(Field);
+ m_Builder << '\n';
}
+ }
- if (std::u8string_view Name = Field.GetU8Name(); !Name.empty())
+private:
+ void WriteMapEntries(CbFieldView MapField, int32_t Indent)
+ {
+ for (CbFieldView Child : MapField)
{
- Node.set_key_serialized(ryml::csubstr((const char*)Name.data(), Name.size()));
+ WriteIndent(Indent);
+ WriteMapEntryContent(Child, Indent);
}
+ }
+
+ void WriteMapEntryContent(CbFieldView Field, int32_t Indent)
+ {
+ std::u8string_view Name = Field.GetU8Name();
+ m_Builder << std::string_view(reinterpret_cast<const char*>(Name.data()), Name.size());
- switch (CbValue Accessor = Field.GetValue(); Accessor.GetType())
+ CbValue Accessor = Field.GetValue();
+ CbFieldType Type = Accessor.GetType();
+
+ if (IsContainer(Type))
{
- case CbFieldType::Null:
- Node.set_val("null");
- break;
- case CbFieldType::Object:
- case CbFieldType::UniformObject:
- Node |= ryml::MAP;
- m_NodeStack.push_back(Node);
- for (CbFieldView It : Field)
+ m_Builder << ":\n";
+ WriteFieldValue(Field, Indent + 1);
+ }
+ else
+ {
+ m_Builder << ": ";
+ WriteScalarValue(Field);
+ m_Builder << '\n';
+ }
+ }
+
+ void WriteSeqEntries(CbFieldView SeqField, int32_t Indent)
+ {
+ for (CbFieldView Child : SeqField)
+ {
+ CbValue Accessor = Child.GetValue();
+ CbFieldType Type = Accessor.GetType();
+
+ if (Type == CbFieldType::Object || Type == CbFieldType::UniformObject)
+ {
+ bool First = true;
+ for (CbFieldView MapChild : Child)
{
- WriteField(It);
+ if (First)
+ {
+ WriteIndent(Indent);
+ m_Builder << "- ";
+ First = false;
+ }
+ else
+ {
+ WriteIndent(Indent + 1);
+ }
+ WriteMapEntryContent(MapChild, Indent + 1);
}
- m_NodeStack.pop_back();
+ }
+ else if (Type == CbFieldType::Array || Type == CbFieldType::UniformArray)
+ {
+ WriteIndent(Indent);
+ m_Builder << "-\n";
+ WriteSeqEntries(Child, Indent + 1);
+ }
+ else
+ {
+ WriteIndent(Indent);
+ m_Builder << "- ";
+ WriteScalarValue(Child);
+ m_Builder << '\n';
+ }
+ }
+ }
+
+ void WriteFieldValue(CbFieldView Field, int32_t Indent)
+ {
+ CbValue Accessor = Field.GetValue();
+ CbFieldType Type = Accessor.GetType();
+
+ switch (Type)
+ {
+ case CbFieldType::Object:
+ case CbFieldType::UniformObject:
+ WriteMapEntries(Field, Indent);
break;
case CbFieldType::Array:
case CbFieldType::UniformArray:
- Node |= ryml::SEQ;
- m_NodeStack.push_back(Node);
- for (CbFieldView It : Field)
- {
- WriteField(It);
- }
- m_NodeStack.pop_back();
+ WriteSeqEntries(Field, Indent);
break;
- case CbFieldType::Binary:
- {
- ExtendableStringBuilder<256> Builder;
- const MemoryView Value = Accessor.AsBinary();
- ZEN_ASSERT(Value.GetSize() <= 512 * 1024 * 1024);
- const uint32_t EncodedSize = Base64::GetEncodedDataSize(uint32_t(Value.GetSize()));
- const size_t EncodedIndex = Builder.AddUninitialized(size_t(EncodedSize));
- Base64::Encode(static_cast<const uint8_t*>(Value.GetData()), uint32_t(Value.GetSize()), Builder.Data() + EncodedIndex);
-
- Node.set_key_serialized(Builder.c_str());
- }
+ case CbFieldType::CustomById:
+ WriteCustomById(Field.GetValue().AsCustomById(), Indent);
break;
- case CbFieldType::String:
- {
- const std::u8string_view U8String = Accessor.AsU8String();
- Node.set_val(ryml::csubstr((const char*)U8String.data(), U8String.size()));
- }
+ case CbFieldType::CustomByName:
+ WriteCustomByName(Field.GetValue().AsCustomByName(), Indent);
+ break;
+ default:
+ WriteScalarValue(Field);
+ m_Builder << '\n';
+ break;
+ }
+ }
+
+ void WriteScalarValue(CbFieldView Field)
+ {
+ CbValue Accessor = Field.GetValue();
+ switch (Accessor.GetType())
+ {
+ case CbFieldType::Null:
+ m_Builder << "null";
+ break;
+ case CbFieldType::BoolFalse:
+ m_Builder << "false";
+ break;
+ case CbFieldType::BoolTrue:
+ m_Builder << "true";
break;
case CbFieldType::IntegerPositive:
- Node << Accessor.AsIntegerPositive();
+ m_Builder << Accessor.AsIntegerPositive();
break;
case CbFieldType::IntegerNegative:
- Node << Accessor.AsIntegerNegative();
+ m_Builder << Accessor.AsIntegerNegative();
break;
case CbFieldType::Float32:
if (const float Value = Accessor.AsFloat32(); std::isfinite(Value))
- {
- Node << Value;
- }
+ m_Builder.Append(fmt::format("{}", Value));
else
- {
- Node << "null";
- }
+ m_Builder << "null";
break;
case CbFieldType::Float64:
if (const double Value = Accessor.AsFloat64(); std::isfinite(Value))
- {
- Node << Value;
- }
+ m_Builder.Append(fmt::format("{}", Value));
else
+ m_Builder << "null";
+ break;
+ case CbFieldType::String:
{
- Node << "null";
+ const std::u8string_view U8String = Accessor.AsU8String();
+ WriteString(std::string_view(reinterpret_cast<const char*>(U8String.data()), U8String.size()));
}
break;
- case CbFieldType::BoolFalse:
- Node << "false";
- break;
- case CbFieldType::BoolTrue:
- Node << "true";
+ case CbFieldType::Hash:
+ WriteString(Accessor.AsHash().ToHexString());
break;
case CbFieldType::ObjectAttachment:
case CbFieldType::BinaryAttachment:
- Node << Accessor.AsAttachment().ToHexString();
- break;
- case CbFieldType::Hash:
- Node << Accessor.AsHash().ToHexString();
+ WriteString(Accessor.AsAttachment().ToHexString());
break;
case CbFieldType::Uuid:
- Node << fmt::format("{}", Accessor.AsUuid());
+ WriteString(fmt::format("{}", Accessor.AsUuid()));
break;
case CbFieldType::DateTime:
- Node << DateTime(Accessor.AsDateTimeTicks()).ToIso8601();
+ WriteString(DateTime(Accessor.AsDateTimeTicks()).ToIso8601());
break;
case CbFieldType::TimeSpan:
if (const TimeSpan Span(Accessor.AsTimeSpanTicks()); Span.GetDays() == 0)
- {
- Node << Span.ToString("%h:%m:%s.%n");
- }
+ WriteString(Span.ToString("%h:%m:%s.%n"));
else
- {
- Node << Span.ToString("%d.%h:%m:%s.%n");
- }
+ WriteString(Span.ToString("%d.%h:%m:%s.%n"));
break;
case CbFieldType::ObjectId:
- Node << fmt::format("{}", Accessor.AsObjectId());
+ WriteString(fmt::format("{}", Accessor.AsObjectId()));
break;
- case CbFieldType::CustomById:
- {
- CbCustomById Custom = Accessor.AsCustomById();
+ case CbFieldType::Binary:
+ WriteBase64(Accessor.AsBinary());
+ break;
+ default:
+ ZEN_ASSERT_FORMAT(false, "invalid field type: {}", uint8_t(Accessor.GetType()));
+ break;
+ }
+ }
- Node |= ryml::MAP;
+ void WriteCustomById(CbCustomById Custom, int32_t Indent)
+ {
+ WriteIndent(Indent);
+ m_Builder << "Id: ";
+ m_Builder.Append(fmt::format("{}", Custom.Id));
+ m_Builder << '\n';
+
+ WriteIndent(Indent);
+ m_Builder << "Data: ";
+ WriteBase64(Custom.Data);
+ m_Builder << '\n';
+ }
- ryml::NodeRef IdNode = Node.append_child();
- IdNode.set_key("Id");
- IdNode.set_val_serialized(fmt::format("{}", Custom.Id));
+ void WriteCustomByName(CbCustomByName Custom, int32_t Indent)
+ {
+ WriteIndent(Indent);
+ m_Builder << "Name: ";
+ WriteString(std::string_view(reinterpret_cast<const char*>(Custom.Name.data()), Custom.Name.size()));
+ m_Builder << '\n';
+
+ WriteIndent(Indent);
+ m_Builder << "Data: ";
+ WriteBase64(Custom.Data);
+ m_Builder << '\n';
+ }
- ryml::NodeRef DataNode = Node.append_child();
- DataNode.set_key("Data");
+ void WriteBase64(MemoryView Value)
+ {
+ ZEN_ASSERT(Value.GetSize() <= 512 * 1024 * 1024);
+ ExtendableStringBuilder<256> Buf;
+ const uint32_t EncodedSize = Base64::GetEncodedDataSize(uint32_t(Value.GetSize()));
+ const size_t EncodedIndex = Buf.AddUninitialized(size_t(EncodedSize));
+ Base64::Encode(static_cast<const uint8_t*>(Value.GetData()), uint32_t(Value.GetSize()), Buf.Data() + EncodedIndex);
+ WriteString(Buf.ToView());
+ }
- ExtendableStringBuilder<256> Builder;
- const MemoryView& Value = Custom.Data;
- const uint32_t EncodedSize = Base64::GetEncodedDataSize(uint32_t(Value.GetSize()));
- const size_t EncodedIndex = Builder.AddUninitialized(size_t(EncodedSize));
- Base64::Encode(static_cast<const uint8_t*>(Value.GetData()), uint32_t(Value.GetSize()), Builder.Data() + EncodedIndex);
+ void WriteString(std::string_view Str)
+ {
+ if (NeedsQuoting(Str))
+ {
+ m_Builder << '\'';
+ for (char C : Str)
+ {
+ if (C == '\'')
+ m_Builder << "''";
+ else
+ m_Builder << C;
+ }
+ m_Builder << '\'';
+ }
+ else
+ {
+ m_Builder << Str;
+ }
+ }
- DataNode.set_val_serialized(Builder.c_str());
- }
- break;
- case CbFieldType::CustomByName:
- {
- CbCustomByName Custom = Accessor.AsCustomByName();
+ void WriteIndent(int32_t Indent)
+ {
+ for (int32_t I = 0; I < Indent; ++I)
+ m_Builder << " ";
+ }
- Node |= ryml::MAP;
+ static bool NeedsQuoting(std::string_view Str)
+ {
+ if (Str.empty())
+ return false;
- ryml::NodeRef NameNode = Node.append_child();
- NameNode.set_key("Name");
- std::string_view Name = std::string_view((const char*)Custom.Name.data(), Custom.Name.size());
- NameNode.set_val_serialized(std::string(Name));
+ char First = Str[0];
+ if (First == ' ' || First == '\n' || First == '\t' || First == '\r' || First == '*' || First == '&' || First == '%' ||
+ First == '@' || First == '`')
+ return true;
- ryml::NodeRef DataNode = Node.append_child();
- DataNode.set_key("Data");
+ if (Str.size() >= 2 && Str[0] == '<' && Str[1] == '<')
+ return true;
- ExtendableStringBuilder<256> Builder;
- const MemoryView& Value = Custom.Data;
- const uint32_t EncodedSize = Base64::GetEncodedDataSize(uint32_t(Value.GetSize()));
- const size_t EncodedIndex = Builder.AddUninitialized(size_t(EncodedSize));
- Base64::Encode(static_cast<const uint8_t*>(Value.GetData()), uint32_t(Value.GetSize()), Builder.Data() + EncodedIndex);
+ char Last = Str.back();
+ if (Last == ' ' || Last == '\n' || Last == '\t' || Last == '\r')
+ return true;
- DataNode.set_val_serialized(Builder.c_str());
- }
- break;
- default:
- ZEN_ASSERT_FORMAT(false, "invalid field type: {}", uint8_t(Accessor.GetType()));
- break;
+ for (char C : Str)
+ {
+ if (C == '#' || C == ':' || C == '-' || C == '?' || C == ',' || C == '\n' || C == '{' || C == '}' || C == '[' || C == ']' ||
+ C == '\'' || C == '"')
+ return true;
}
- if (m_NodeStack.size() == 1)
+ return false;
+ }
+
+ static bool IsContainer(CbFieldType Type)
+ {
+ switch (Type)
{
- std::string Yaml = ryml::emitrs_yaml<std::string>(m_Tree);
- m_StrBuilder << Yaml;
+ case CbFieldType::Object:
+ case CbFieldType::UniformObject:
+ case CbFieldType::Array:
+ case CbFieldType::UniformArray:
+ case CbFieldType::CustomById:
+ case CbFieldType::CustomByName:
+ return true;
+ default:
+ return false;
}
}
-private:
- StringBuilderBase& m_StrBuilder;
- bool m_IsFirst = true;
-
- ryml::Tree m_Tree;
- std::vector<ryml::NodeRef> m_NodeStack;
- ryml::NodeRef& Top() { return m_NodeStack.back(); }
+ StringBuilderBase& m_Builder;
};
void
@@ -229,6 +380,32 @@ CompactBinaryToYaml(const CbArrayView& Array, StringBuilderBase& Builder)
Writer.WriteField(Array.AsFieldView());
}
+void
+CompactBinaryToYaml(MemoryView Data, StringBuilderBase& InBuilder)
+{
+ std::vector<CbFieldView> Fields = ReadCompactBinaryStream(Data);
+ if (Fields.empty())
+ return;
+
+ CbYamlWriter Writer(InBuilder);
+ if (Fields.size() == 1)
+ {
+ Writer.WriteField(Fields[0]);
+ return;
+ }
+
+ if (Fields[0].HasName())
+ {
+ for (const CbFieldView& Field : Fields)
+ Writer.WriteMapEntry(Field, 0);
+ }
+ else
+ {
+ for (const CbFieldView& Field : Fields)
+ Writer.WriteSeqEntry(Field, 0);
+ }
+}
+
#if ZEN_WITH_TESTS
void
cbyaml_forcelink()
diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp
index 92a065707..553897407 100644
--- a/src/zencore/filesystem.cpp
+++ b/src/zencore/filesystem.cpp
@@ -1326,11 +1326,6 @@ ReadFile(void* NativeHandle, void* Data, uint64_t Size, uint64_t FileOffset, uin
{
BytesRead = size_t(dwNumberOfBytesRead);
}
- else if ((BytesRead != NumberOfBytesToRead))
- {
- Ec = MakeErrorCode(ERROR_HANDLE_EOF);
- return;
- }
else
{
Ec = MakeErrorCodeFromLastError();
@@ -1344,20 +1339,15 @@ ReadFile(void* NativeHandle, void* Data, uint64_t Size, uint64_t FileOffset, uin
{
BytesRead = size_t(ReadResult);
}
- else if ((BytesRead != NumberOfBytesToRead))
- {
- Ec = MakeErrorCode(EIO);
- return;
- }
else
{
Ec = MakeErrorCodeFromLastError();
return;
}
#endif
- Size -= NumberOfBytesToRead;
- FileOffset += NumberOfBytesToRead;
- Data = reinterpret_cast<uint8_t*>(Data) + NumberOfBytesToRead;
+ Size -= BytesRead;
+ FileOffset += BytesRead;
+ Data = reinterpret_cast<uint8_t*>(Data) + BytesRead;
}
}
@@ -3069,7 +3059,7 @@ SetFileReadOnly(const std::filesystem::path& Filename, bool ReadOnly)
}
void
-MakeSafeAbsolutePathÍnPlace(std::filesystem::path& Path)
+MakeSafeAbsolutePathInPlace(std::filesystem::path& Path)
{
if (!Path.empty())
{
@@ -3091,7 +3081,7 @@ std::filesystem::path
MakeSafeAbsolutePath(const std::filesystem::path& Path)
{
std::filesystem::path Tmp(Path);
- MakeSafeAbsolutePathÍnPlace(Tmp);
+ MakeSafeAbsolutePathInPlace(Tmp);
return Tmp;
}
diff --git a/src/zencore/include/zencore/base64.h b/src/zencore/include/zencore/base64.h
index 4d78b085f..08d9f3043 100644
--- a/src/zencore/include/zencore/base64.h
+++ b/src/zencore/include/zencore/base64.h
@@ -11,7 +11,11 @@ struct Base64
template<typename CharType>
static uint32_t Encode(const uint8_t* Source, uint32_t Length, CharType* Dest);
+ template<typename CharType>
+ static bool Decode(const CharType* Source, uint32_t Length, uint8_t* Dest, uint32_t& OutLength);
+
static inline constexpr int32_t GetEncodedDataSize(uint32_t Size) { return ((Size + 2) / 3) * 4; }
+ static inline constexpr int32_t GetMaxDecodedDataSize(uint32_t Length) { return (Length / 4) * 3; }
};
} // namespace zen
diff --git a/src/zencore/include/zencore/compactbinaryvalue.h b/src/zencore/include/zencore/compactbinaryvalue.h
index aa2d2821d..4ce8009b8 100644
--- a/src/zencore/include/zencore/compactbinaryvalue.h
+++ b/src/zencore/include/zencore/compactbinaryvalue.h
@@ -128,17 +128,21 @@ CbValue::AsString(CbFieldError* OutError, std::string_view Default) const
uint32_t ValueSizeByteCount;
const uint64_t ValueSize = ReadVarUInt(Chars, ValueSizeByteCount);
- if (OutError)
+ if (ValueSize >= (uint64_t(1) << 31))
{
- if (ValueSize >= (uint64_t(1) << 31))
+ if (OutError)
{
*OutError = CbFieldError::RangeError;
- return Default;
}
+ return Default;
+ }
+
+ if (OutError)
+ {
*OutError = CbFieldError::None;
}
- return std::string_view(Chars + ValueSizeByteCount, int32_t(ValueSize));
+ return std::string_view(Chars + ValueSizeByteCount, size_t(ValueSize));
}
inline std::u8string_view
@@ -148,17 +152,21 @@ CbValue::AsU8String(CbFieldError* OutError, std::u8string_view Default) const
uint32_t ValueSizeByteCount;
const uint64_t ValueSize = ReadVarUInt(Chars, ValueSizeByteCount);
- if (OutError)
+ if (ValueSize >= (uint64_t(1) << 31))
{
- if (ValueSize >= (uint64_t(1) << 31))
+ if (OutError)
{
*OutError = CbFieldError::RangeError;
- return Default;
}
+ return Default;
+ }
+
+ if (OutError)
+ {
*OutError = CbFieldError::None;
}
- return std::u8string_view(Chars + ValueSizeByteCount, int32_t(ValueSize));
+ return std::u8string_view(Chars + ValueSizeByteCount, size_t(ValueSize));
}
inline uint64_t
diff --git a/src/zencore/include/zencore/filesystem.h b/src/zencore/include/zencore/filesystem.h
index f28863679..16e2b59f8 100644
--- a/src/zencore/include/zencore/filesystem.h
+++ b/src/zencore/include/zencore/filesystem.h
@@ -64,80 +64,80 @@ std::filesystem::path PathFromHandle(void* NativeHandle, std::error_code& Ec);
*/
std::filesystem::path CanonicalPath(std::filesystem::path InPath, std::error_code& Ec);
-/** Query file size
+/** Check if a path exists and is a regular file (throws)
*/
bool IsFile(const std::filesystem::path& Path);
-/** Query file size
+/** Check if a path exists and is a regular file (does not throw)
*/
bool IsFile(const std::filesystem::path& Path, std::error_code& Ec);
-/** Query file size
+/** Check if a path exists and is a directory (throws)
*/
bool IsDir(const std::filesystem::path& Path);
-/** Query file size
+/** Check if a path exists and is a directory (does not throw)
*/
bool IsDir(const std::filesystem::path& Path, std::error_code& Ec);
-/** Query file size
+/** Delete file at path, if it exists (throws)
*/
bool RemoveFile(const std::filesystem::path& Path);
-/** Query file size
+/** Delete file at path, if it exists (does not throw)
*/
bool RemoveFile(const std::filesystem::path& Path, std::error_code& Ec);
-/** Query file size
+/** Delete directory at path, if it exists (throws)
*/
bool RemoveDir(const std::filesystem::path& Path);
-/** Query file size
+/** Delete directory at path, if it exists (does not throw)
*/
bool RemoveDir(const std::filesystem::path& Path, std::error_code& Ec);
-/** Query file size
+/** Query file size (throws)
*/
uint64_t FileSizeFromPath(const std::filesystem::path& Path);
-/** Query file size
+/** Query file size (does not throw)
*/
uint64_t FileSizeFromPath(const std::filesystem::path& Path, std::error_code& Ec);
-/** Query file size from native file handle
+/** Query file size from native file handle (throws)
*/
uint64_t FileSizeFromHandle(void* NativeHandle);
-/** Query file size from native file handle
+/** Query file size from native file handle (does not throw)
*/
uint64_t FileSizeFromHandle(void* NativeHandle, std::error_code& Ec);
/** Get a native time tick of last modification time
*/
-uint64_t GetModificationTickFromHandle(void* NativeHandle, std::error_code& Ec);
+uint64_t GetModificationTickFromPath(const std::filesystem::path& Filename);
/** Get a native time tick of last modification time
*/
-uint64_t GetModificationTickFromPath(const std::filesystem::path& Filename);
+uint64_t GetModificationTickFromHandle(void* NativeHandle, std::error_code& Ec);
bool TryGetFileProperties(const std::filesystem::path& Path,
uint64_t& OutSize,
uint64_t& OutModificationTick,
uint32_t& OutNativeModeOrAttributes);
-/** Move a file, if the files are not on the same drive the function will fail
+/** Move/rename a file, if the files are not on the same drive the function will fail (throws)
*/
void RenameFile(const std::filesystem::path& SourcePath, const std::filesystem::path& TargetPath);
-/** Move a file, if the files are not on the same drive the function will fail
+/** Move/rename a file, if the files are not on the same drive the function will fail
*/
void RenameFile(const std::filesystem::path& SourcePath, const std::filesystem::path& TargetPath, std::error_code& Ec);
-/** Move a directory, if the files are not on the same drive the function will fail
+/** Move/rename a directory, if the files are not on the same drive the function will fail (throws)
*/
void RenameDirectory(const std::filesystem::path& SourcePath, const std::filesystem::path& TargetPath);
-/** Move a directory, if the files are not on the same drive the function will fail
+/** Move/rename a directory, if the files are not on the same drive the function will fail
*/
void RenameDirectory(const std::filesystem::path& SourcePath, const std::filesystem::path& TargetPath, std::error_code& Ec);
@@ -421,7 +421,7 @@ uint32_t MakeFileModeReadOnly(uint32_t FileMode, bool ReadOnly);
bool SetFileReadOnly(const std::filesystem::path& Filename, bool ReadOnly, std::error_code& Ec);
bool SetFileReadOnly(const std::filesystem::path& Filename, bool ReadOnly);
-void MakeSafeAbsolutePathÍnPlace(std::filesystem::path& Path);
+void MakeSafeAbsolutePathInPlace(std::filesystem::path& Path);
[[nodiscard]] std::filesystem::path MakeSafeAbsolutePath(const std::filesystem::path& Path);
class SharedMemory
diff --git a/src/zencore/include/zencore/hashutils.h b/src/zencore/include/zencore/hashutils.h
index 4e877e219..6b9902b3a 100644
--- a/src/zencore/include/zencore/hashutils.h
+++ b/src/zencore/include/zencore/hashutils.h
@@ -2,6 +2,9 @@
#pragma once
+#include <cstddef>
+#include <type_traits>
+
namespace zen {
template<typename T>
diff --git a/src/zencore/include/zencore/iobuffer.h b/src/zencore/include/zencore/iobuffer.h
index 182768ff6..82c201edd 100644
--- a/src/zencore/include/zencore/iobuffer.h
+++ b/src/zencore/include/zencore/iobuffer.h
@@ -426,22 +426,39 @@ private:
class IoBufferBuilder
{
public:
- static IoBuffer MakeFromFile(const std::filesystem::path& FileName, uint64_t Offset = 0, uint64_t Size = ~0ull);
- static IoBuffer MakeFromTemporaryFile(const std::filesystem::path& FileName);
- static IoBuffer MakeFromFileHandle(void* FileHandle, uint64_t Offset = 0, uint64_t Size = ~0ull);
- /** Make sure buffer data is memory resident, but avoid memory mapping data from files
- */
- static IoBuffer ReadFromFileMaybe(const IoBuffer& InBuffer);
- inline static IoBuffer MakeFromMemory(MemoryView Memory) { return IoBuffer(IoBuffer::Wrap, Memory.GetData(), Memory.GetSize()); }
- inline static IoBuffer MakeCloneFromMemory(const void* Ptr, size_t Sz)
+ static IoBuffer MakeFromFile(const std::filesystem::path& FileName,
+ uint64_t Offset = 0,
+ uint64_t Size = ~0ull,
+ ZenContentType ContentType = ZenContentType::kBinary);
+ static IoBuffer MakeFromTemporaryFile(const std::filesystem::path& FileName, ZenContentType ContentType = ZenContentType::kBinary);
+ static IoBuffer MakeFromFileHandle(void* FileHandle,
+ uint64_t Offset = 0,
+ uint64_t Size = ~0ull,
+ ZenContentType ContentType = ZenContentType::kBinary);
+ inline static IoBuffer MakeFromMemory(MemoryView Memory, ZenContentType ContentType = ZenContentType::kBinary)
+ {
+ IoBuffer NewBuffer(IoBuffer::Wrap, Memory.GetData(), Memory.GetSize());
+ NewBuffer.SetContentType(ContentType);
+ return NewBuffer;
+ }
+ inline static IoBuffer MakeCloneFromMemory(const void* Ptr, size_t Sz, ZenContentType ContentType = ZenContentType::kBinary)
{
if (Sz)
{
- return IoBuffer(IoBuffer::Clone, Ptr, Sz);
+ IoBuffer NewBuffer(IoBuffer::Clone, Ptr, Sz);
+ NewBuffer.SetContentType(ContentType);
+ return NewBuffer;
}
return {};
}
- inline static IoBuffer MakeCloneFromMemory(MemoryView Memory) { return MakeCloneFromMemory(Memory.GetData(), Memory.GetSize()); }
+ inline static IoBuffer MakeCloneFromMemory(MemoryView Memory, ZenContentType ContentType = ZenContentType::kBinary)
+ {
+ return MakeCloneFromMemory(Memory.GetData(), Memory.GetSize(), ContentType);
+ }
+
+ /** Make sure buffer data is memory resident, but avoid memory mapping data from files
+ */
+ static IoBuffer ReadFromFileMaybe(const IoBuffer& InBuffer);
};
void iobuffer_forcelink();
diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h
index e3b7a70d7..c51163a68 100644
--- a/src/zencore/include/zencore/process.h
+++ b/src/zencore/include/zencore/process.h
@@ -105,6 +105,7 @@ int GetCurrentProcessId();
int GetProcessId(CreateProcResult ProcId);
std::filesystem::path GetProcessExecutablePath(int Pid, std::error_code& OutEc);
+std::string GetProcessCommandLine(int Pid, std::error_code& OutEc);
std::error_code FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHandle, bool IncludeSelf = true);
/** Wait for all threads in the current process to exit (except the calling thread)
diff --git a/src/zencore/include/zencore/sharedbuffer.h b/src/zencore/include/zencore/sharedbuffer.h
index c57e9f568..3d4c19282 100644
--- a/src/zencore/include/zencore/sharedbuffer.h
+++ b/src/zencore/include/zencore/sharedbuffer.h
@@ -116,14 +116,15 @@ public:
inline void Reset() { m_Buffer = nullptr; }
inline bool GetFileReference(IoBufferFileReference& OutRef) const
{
- if (const IoBufferExtendedCore* Core = m_Buffer->ExtendedCore())
+ if (!IsNull())
{
- return Core->GetFileReference(OutRef);
- }
- else
- {
- return false;
+ if (const IoBufferExtendedCore* Core = m_Buffer->ExtendedCore())
+ {
+ return Core->GetFileReference(OutRef);
+ }
}
+
+ return false;
}
[[nodiscard]] MemoryView GetView() const
diff --git a/src/zencore/intmath.cpp b/src/zencore/intmath.cpp
index 5a686dc8e..32f82b486 100644
--- a/src/zencore/intmath.cpp
+++ b/src/zencore/intmath.cpp
@@ -43,6 +43,12 @@ TEST_CASE("intmath")
CHECK(FloorLog2_64(0x0000'0001'0000'0000ull) == 32);
CHECK(FloorLog2_64(0x8000'0000'0000'0000ull) == 63);
+ CHECK(CountLeadingZeros(0x8000'0000u) == 0);
+ CHECK(CountLeadingZeros(0x0000'0000u) == 32);
+ CHECK(CountLeadingZeros(0x0000'0001u) == 31);
+ CHECK(CountLeadingZeros(0x0000'8000u) == 16);
+ CHECK(CountLeadingZeros(0x0001'0000u) == 15);
+
CHECK(CountLeadingZeros64(0x8000'0000'0000'0000ull) == 0);
CHECK(CountLeadingZeros64(0x0000'0000'0000'0000ull) == 64);
CHECK(CountLeadingZeros64(0x0000'0000'0000'0001ull) == 63);
diff --git a/src/zencore/iobuffer.cpp b/src/zencore/iobuffer.cpp
index be9b39e7a..1c31d6620 100644
--- a/src/zencore/iobuffer.cpp
+++ b/src/zencore/iobuffer.cpp
@@ -592,15 +592,17 @@ IoBufferBuilder::ReadFromFileMaybe(const IoBuffer& InBuffer)
}
IoBuffer
-IoBufferBuilder::MakeFromFileHandle(void* FileHandle, uint64_t Offset, uint64_t Size)
+IoBufferBuilder::MakeFromFileHandle(void* FileHandle, uint64_t Offset, uint64_t Size, ZenContentType ContentType)
{
ZEN_TRACE_CPU("IoBufferBuilder::MakeFromFileHandle");
- return IoBuffer(IoBuffer::BorrowedFile, FileHandle, Offset, Size);
+ IoBuffer Buffer(IoBuffer::BorrowedFile, FileHandle, Offset, Size);
+ Buffer.SetContentType(ContentType);
+ return Buffer;
}
IoBuffer
-IoBufferBuilder::MakeFromFile(const std::filesystem::path& FileName, uint64_t Offset, uint64_t Size)
+IoBufferBuilder::MakeFromFile(const std::filesystem::path& FileName, uint64_t Offset, uint64_t Size, ZenContentType ContentType)
{
ZEN_TRACE_CPU("IoBufferBuilder::MakeFromFile");
@@ -632,8 +634,6 @@ IoBufferBuilder::MakeFromFile(const std::filesystem::path& FileName, uint64_t Of
FileSize = Stat.st_size;
#endif // ZEN_PLATFORM_WINDOWS
- // TODO: should validate that offset is in range
-
if (Size == ~0ull)
{
Size = FileSize - Offset;
@@ -652,7 +652,9 @@ IoBufferBuilder::MakeFromFile(const std::filesystem::path& FileName, uint64_t Of
#if ZEN_PLATFORM_WINDOWS
void* Fd = DataFile.Detach();
#endif
- return IoBuffer(IoBuffer::File, (void*)uintptr_t(Fd), Offset, Size, Offset == 0 && Size == FileSize);
+ IoBuffer NewBuffer(IoBuffer::File, (void*)uintptr_t(Fd), Offset, Size, Offset == 0 && Size == FileSize);
+ NewBuffer.SetContentType(ContentType);
+ return NewBuffer;
}
#if !ZEN_PLATFORM_WINDOWS
@@ -664,7 +666,7 @@ IoBufferBuilder::MakeFromFile(const std::filesystem::path& FileName, uint64_t Of
}
IoBuffer
-IoBufferBuilder::MakeFromTemporaryFile(const std::filesystem::path& FileName)
+IoBufferBuilder::MakeFromTemporaryFile(const std::filesystem::path& FileName, ZenContentType ContentType)
{
ZEN_TRACE_CPU("IoBufferBuilder::MakeFromTemporaryFile");
@@ -703,7 +705,9 @@ IoBufferBuilder::MakeFromTemporaryFile(const std::filesystem::path& FileName)
Handle = (void*)uintptr_t(Fd);
#endif // ZEN_PLATFORM_WINDOWS
- return IoBuffer(IoBuffer::File, Handle, 0, FileSize, /*IsWholeFile*/ true);
+ IoBuffer NewBuffer(IoBuffer::File, Handle, 0, FileSize, /*IsWholeFile*/ true);
+ NewBuffer.SetContentType(ContentType);
+ return NewBuffer;
}
//////////////////////////////////////////////////////////////////////////
diff --git a/src/zencore/memtrack/callstacktrace.cpp b/src/zencore/memtrack/callstacktrace.cpp
index a5b7fede6..4a7068568 100644
--- a/src/zencore/memtrack/callstacktrace.cpp
+++ b/src/zencore/memtrack/callstacktrace.cpp
@@ -169,13 +169,13 @@ private:
std::atomic_uint64_t Key;
std::atomic_uint32_t Value;
- inline uint64 GetKey() const { return Key.load(std::memory_order_relaxed); }
+ inline uint64 GetKey() const { return Key.load(std::memory_order_acquire); }
inline uint32_t GetValue() const { return Value.load(std::memory_order_relaxed); }
- inline bool IsEmpty() const { return Key.load(std::memory_order_relaxed) == 0; }
+ inline bool IsEmpty() const { return Key.load(std::memory_order_acquire) == 0; }
inline void SetKeyValue(uint64_t InKey, uint32_t InValue)
{
- Value.store(InValue, std::memory_order_release);
- Key.store(InKey, std::memory_order_relaxed);
+ Value.store(InValue, std::memory_order_relaxed);
+ Key.store(InKey, std::memory_order_release);
}
static inline uint32_t KeyHash(uint64_t Key) { return static_cast<uint32_t>(Key); }
static inline void ClearEntries(FEncounteredCallstackSetEntry* Entries, int32_t EntryCount)
diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp
index 56849a10d..4a2668912 100644
--- a/src/zencore/process.cpp
+++ b/src/zencore/process.cpp
@@ -1001,6 +1001,232 @@ GetProcessExecutablePath(int Pid, std::error_code& OutEc)
#endif // ZEN_PLATFORM_LINUX
}
+std::string
+GetProcessCommandLine(int Pid, std::error_code& OutEc)
+{
+#if ZEN_PLATFORM_WINDOWS
+ HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, static_cast<DWORD>(Pid));
+ if (!hProcess)
+ {
+ OutEc = MakeErrorCodeFromLastError();
+ return {};
+ }
+ auto _ = MakeGuard([hProcess] { CloseHandle(hProcess); });
+
+ // NtQueryInformationProcess is an undocumented NT API; load it dynamically.
+ // Info class 60 = ProcessCommandLine, available since Windows 8.1.
+ using PFN_NtQIP = LONG(WINAPI*)(HANDLE, UINT, PVOID, ULONG, PULONG);
+ static const PFN_NtQIP s_NtQIP =
+ reinterpret_cast<PFN_NtQIP>(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationProcess"));
+ if (!s_NtQIP)
+ {
+ return {};
+ }
+
+ constexpr UINT ProcessCommandLineClass = 60;
+ constexpr LONG StatusInfoLengthMismatch = static_cast<LONG>(0xC0000004L);
+
+ ULONG ReturnLength = 0;
+ LONG Status = s_NtQIP(hProcess, ProcessCommandLineClass, nullptr, 0, &ReturnLength);
+ if (Status != StatusInfoLengthMismatch || ReturnLength == 0)
+ {
+ return {};
+ }
+
+ std::vector<char> Buf(ReturnLength);
+ Status = s_NtQIP(hProcess, ProcessCommandLineClass, Buf.data(), ReturnLength, &ReturnLength);
+ if (Status < 0)
+ {
+ OutEc = MakeErrorCodeFromLastError();
+ return {};
+ }
+
+ // Output: UNICODE_STRING header immediately followed by the UTF-16 string data.
+ // The UNICODE_STRING.Buffer field points into our Buf.
+ struct LocalUnicodeString
+ {
+ USHORT Length;
+ USHORT MaximumLength;
+ WCHAR* Buffer;
+ };
+ if (ReturnLength < sizeof(LocalUnicodeString))
+ {
+ return {};
+ }
+ const auto* Us = reinterpret_cast<const LocalUnicodeString*>(Buf.data());
+ if (Us->Length == 0 || Us->Buffer == nullptr)
+ {
+ return {};
+ }
+
+ // Skip argv[0]: may be a quoted path ("C:\...\exe.exe") or a bare path
+ const WCHAR* p = Us->Buffer;
+ const WCHAR* End = Us->Buffer + Us->Length / sizeof(WCHAR);
+ if (p < End && *p == L'"')
+ {
+ ++p;
+ while (p < End && *p != L'"')
+ {
+ ++p;
+ }
+ if (p < End)
+ {
+ ++p; // skip closing quote
+ }
+ }
+ else
+ {
+ while (p < End && *p != L' ')
+ {
+ ++p;
+ }
+ }
+ while (p < End && *p == L' ')
+ {
+ ++p;
+ }
+ if (p >= End)
+ {
+ return {};
+ }
+
+ int Utf8Size = WideCharToMultiByte(CP_UTF8, 0, p, static_cast<int>(End - p), nullptr, 0, nullptr, nullptr);
+ if (Utf8Size <= 0)
+ {
+ OutEc = MakeErrorCodeFromLastError();
+ return {};
+ }
+ std::string Result(Utf8Size, '\0');
+ WideCharToMultiByte(CP_UTF8, 0, p, static_cast<int>(End - p), Result.data(), Utf8Size, nullptr, nullptr);
+ return Result;
+
+#elif ZEN_PLATFORM_LINUX
+ std::string CmdlinePath = fmt::format("/proc/{}/cmdline", Pid);
+ FILE* F = fopen(CmdlinePath.c_str(), "rb");
+ if (!F)
+ {
+ OutEc = MakeErrorCodeFromLastError();
+ return {};
+ }
+ auto FGuard = MakeGuard([F] { fclose(F); });
+
+ // /proc/{pid}/cmdline contains null-separated argv entries; read it all
+ std::string Raw;
+ char Chunk[4096];
+ size_t BytesRead;
+ while ((BytesRead = fread(Chunk, 1, sizeof(Chunk), F)) > 0)
+ {
+ Raw.append(Chunk, BytesRead);
+ }
+ if (Raw.empty())
+ {
+ return {};
+ }
+
+ // Skip argv[0] (first null-terminated entry)
+ const char* p = Raw.data();
+ const char* End = Raw.data() + Raw.size();
+ while (p < End && *p != '\0')
+ {
+ ++p;
+ }
+ if (p < End)
+ {
+ ++p; // skip null terminator of argv[0]
+ }
+
+ // Build result: remaining entries joined by spaces (inter-arg nulls → spaces)
+ std::string Result;
+ Result.reserve(static_cast<size_t>(End - p));
+ for (const char* q = p; q < End; ++q)
+ {
+ Result += (*q == '\0') ? ' ' : *q;
+ }
+ while (!Result.empty() && Result.back() == ' ')
+ {
+ Result.pop_back();
+ }
+ return Result;
+
+#elif ZEN_PLATFORM_MAC
+ int Mib[3] = {CTL_KERN, KERN_PROCARGS2, Pid};
+ size_t BufSize = 0;
+ if (sysctl(Mib, 3, nullptr, &BufSize, nullptr, 0) != 0 || BufSize == 0)
+ {
+ OutEc = MakeErrorCodeFromLastError();
+ return {};
+ }
+
+ std::vector<char> Buf(BufSize);
+ if (sysctl(Mib, 3, Buf.data(), &BufSize, nullptr, 0) != 0)
+ {
+ OutEc = MakeErrorCodeFromLastError();
+ return {};
+ }
+
+ // Layout: [int argc][exec_path\0][null padding][argv[0]\0][argv[1]\0]...[envp\0]...
+ if (BufSize < sizeof(int))
+ {
+ return {};
+ }
+ int Argc = 0;
+ memcpy(&Argc, Buf.data(), sizeof(int));
+ if (Argc <= 1)
+ {
+ return {};
+ }
+
+ const char* p = Buf.data() + sizeof(int);
+ const char* End = Buf.data() + BufSize;
+
+ // Skip exec_path and any trailing null padding that follows it
+ while (p < End && *p != '\0')
+ {
+ ++p;
+ }
+ while (p < End && *p == '\0')
+ {
+ ++p;
+ }
+
+ // Skip argv[0]
+ while (p < End && *p != '\0')
+ {
+ ++p;
+ }
+ if (p < End)
+ {
+ ++p;
+ }
+
+ // Collect argv[1..Argc-1]
+ std::string Result;
+ for (int i = 1; i < Argc && p < End; ++i)
+ {
+ if (i > 1)
+ {
+ Result += ' ';
+ }
+ const char* ArgStart = p;
+ while (p < End && *p != '\0')
+ {
+ ++p;
+ }
+ Result.append(ArgStart, p);
+ if (p < End)
+ {
+ ++p;
+ }
+ }
+ return Result;
+
+#else
+ ZEN_UNUSED(Pid);
+ ZEN_UNUSED(OutEc);
+ return {};
+#endif
+}
+
std::error_code
FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHandle, bool IncludeSelf)
{
diff --git a/src/zencore/string.cpp b/src/zencore/string.cpp
index 0ee863b74..a9aed6309 100644
--- a/src/zencore/string.cpp
+++ b/src/zencore/string.cpp
@@ -24,6 +24,10 @@ utf16to8_impl(u16bit_iterator StartIt, u16bit_iterator EndIt, ::zen::StringBuild
// Take care of surrogate pairs first
if (utf8::internal::is_lead_surrogate(cp))
{
+ if (StartIt == EndIt)
+ {
+ break;
+ }
uint32_t trail_surrogate = utf8::internal::mask16(*StartIt++);
cp = (cp << 10) + trail_surrogate + utf8::internal::SURROGATE_OFFSET;
}
diff --git a/src/zencore/trace.cpp b/src/zencore/trace.cpp
index 87035554f..a026974c0 100644
--- a/src/zencore/trace.cpp
+++ b/src/zencore/trace.cpp
@@ -165,10 +165,17 @@ GetTraceOptionsFromCommandline(TraceOptions& OutOptions)
auto MatchesArg = [](std::string_view Option, std::string_view Arg) -> std::optional<std::string_view> {
if (Arg.starts_with(Option))
{
- std::string_view::value_type DelimChar = Arg[Option.length()];
- if (DelimChar == ' ' || DelimChar == '=')
+ if (Arg.length() > Option.length())
{
- return Arg.substr(Option.size() + 1);
+ std::string_view::value_type DelimChar = Arg[Option.length()];
+ if (DelimChar == ' ' || DelimChar == '=')
+ {
+ return Arg.substr(Option.size() + 1);
+ }
+ }
+ else
+ {
+ return ""sv;
}
}
return {};
diff --git a/src/zencore/xmake.lua b/src/zencore/xmake.lua
index a3fd4dacb..9a67175a0 100644
--- a/src/zencore/xmake.lua
+++ b/src/zencore/xmake.lua
@@ -33,8 +33,6 @@ target('zencore')
add_deps("timesinceprocessstart")
add_deps("doctest")
add_deps("fmt")
- add_deps("ryml")
-
add_packages("json11")
if is_plat("linux", "macosx") then
diff --git a/src/zenhttp/httpclient.cpp b/src/zenhttp/httpclient.cpp
index d3b59df2b..078e27b34 100644
--- a/src/zenhttp/httpclient.cpp
+++ b/src/zenhttp/httpclient.cpp
@@ -21,6 +21,8 @@
#include "clients/httpclientcommon.h"
+#include <numeric>
+
#if ZEN_WITH_TESTS
# include <zencore/scopeguard.h>
# include <zencore/testing.h>
@@ -340,6 +342,42 @@ HttpClient::Authenticate()
return m_Inner->Authenticate();
}
+LatencyTestResult
+MeasureLatency(HttpClient& Client, std::string_view Url)
+{
+ std::vector<double> MeasurementTimes;
+ std::string ErrorMessage;
+
+ for (uint32_t AttemptCount = 0; AttemptCount < 20 && MeasurementTimes.size() < 5; AttemptCount++)
+ {
+ HttpClient::Response MeasureResponse = Client.Get(Url);
+ if (MeasureResponse.IsSuccess())
+ {
+ MeasurementTimes.push_back(MeasureResponse.ElapsedSeconds);
+ Sleep(5);
+ }
+ else
+ {
+ ErrorMessage = MeasureResponse.ErrorMessage(fmt::format("Unable to measure latency using {}", Url));
+ }
+ }
+
+ if (MeasurementTimes.empty())
+ {
+ return {.Success = false, .FailureReason = ErrorMessage};
+ }
+
+ if (MeasurementTimes.size() > 2)
+ {
+ std::sort(MeasurementTimes.begin(), MeasurementTimes.end());
+ MeasurementTimes.pop_back(); // Remove the worst time
+ }
+
+ double AverageLatency = std::accumulate(MeasurementTimes.begin(), MeasurementTimes.end(), 0.0) / MeasurementTimes.size();
+
+ return {.Success = true, .LatencySeconds = AverageLatency};
+}
+
//////////////////////////////////////////////////////////////////////////
#if ZEN_WITH_TESTS
diff --git a/src/zenhttp/httpclientauth.cpp b/src/zenhttp/httpclientauth.cpp
index 72df12d02..02e1b57e2 100644
--- a/src/zenhttp/httpclientauth.cpp
+++ b/src/zenhttp/httpclientauth.cpp
@@ -170,7 +170,7 @@ namespace zen { namespace httpclientauth {
time_t UTCTime = timegm(&Time);
HttpClientAccessToken::TimePoint ExpireTime = std::chrono::system_clock::from_time_t(UTCTime);
- ExpireTime += std::chrono::microseconds(Millisecond);
+ ExpireTime += std::chrono::milliseconds(Millisecond);
return HttpClientAccessToken{.Value = fmt::format("Bearer {}"sv, Token), .ExpireTime = ExpireTime};
}
diff --git a/src/zenhttp/httpserver.cpp b/src/zenhttp/httpserver.cpp
index f2fe4738f..3cefa0ad8 100644
--- a/src/zenhttp/httpserver.cpp
+++ b/src/zenhttp/httpserver.cpp
@@ -23,6 +23,7 @@
#include <zencore/logging.h>
#include <zencore/stream.h>
#include <zencore/string.h>
+#include <zencore/system.h>
#include <zencore/testing.h>
#include <zencore/thread.h>
#include <zenhttp/packageformat.h>
@@ -1014,7 +1015,28 @@ HttpRequestRouter::HandleRequest(zen::HttpServerRequest& Request)
int
HttpServer::Initialize(int BasePort, std::filesystem::path DataDir)
{
- return OnInitialize(BasePort, std::move(DataDir));
+ m_EffectivePort = OnInitialize(BasePort, std::move(DataDir));
+ m_ExternalHost = OnGetExternalHost();
+ return m_EffectivePort;
+}
+
+std::string
+HttpServer::OnGetExternalHost() const
+{
+ return GetMachineName();
+}
+
+std::string
+HttpServer::GetServiceUri(const HttpService* Service) const
+{
+ if (Service)
+ {
+ return fmt::format("http://{}:{}{}", m_ExternalHost, m_EffectivePort, Service->BaseUri());
+ }
+ else
+ {
+ return fmt::format("http://{}:{}", m_ExternalHost, m_EffectivePort);
+ }
}
void
diff --git a/src/zenhttp/include/zenhttp/httpclient.h b/src/zenhttp/include/zenhttp/httpclient.h
index 9a9b74d72..7a129a98c 100644
--- a/src/zenhttp/include/zenhttp/httpclient.h
+++ b/src/zenhttp/include/zenhttp/httpclient.h
@@ -260,6 +260,15 @@ private:
const HttpClientSettings m_ConnectionSettings;
};
+struct LatencyTestResult
+{
+ bool Success = false;
+ std::string FailureReason;
+ double LatencySeconds = -1.0;
+};
+
+LatencyTestResult MeasureLatency(HttpClient& Client, std::string_view Url);
+
void httpclient_forcelink(); // internal
} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/httpserver.h b/src/zenhttp/include/zenhttp/httpserver.h
index 350532126..00cbc6c14 100644
--- a/src/zenhttp/include/zenhttp/httpserver.h
+++ b/src/zenhttp/include/zenhttp/httpserver.h
@@ -219,8 +219,21 @@ public:
void RequestExit();
void Close();
+ /** Returns a canonical http:// URI for the given service, using the external
+ * IP and the port the server is actually listening on. Only valid
+ * after Initialize() has returned successfully.
+ */
+ std::string GetServiceUri(const HttpService* Service) const;
+
+ /** Returns the external host string (IP or hostname) determined during Initialize().
+ * Only valid after Initialize() has returned successfully.
+ */
+ std::string_view GetExternalHost() const { return m_ExternalHost; }
+
private:
std::vector<HttpService*> m_KnownServices;
+ int m_EffectivePort = 0;
+ std::string m_ExternalHost;
virtual void OnRegisterService(HttpService& Service) = 0;
virtual int OnInitialize(int BasePort, std::filesystem::path DataDir) = 0;
@@ -228,6 +241,9 @@ private:
virtual void OnRun(bool IsInteractiveSession) = 0;
virtual void OnRequestExit() = 0;
virtual void OnClose() = 0;
+
+protected:
+ virtual std::string OnGetExternalHost() const;
};
struct HttpServerPluginConfig
diff --git a/src/zenhttp/servers/httpasio.cpp b/src/zenhttp/servers/httpasio.cpp
index 1c0ebef90..0c0238886 100644
--- a/src/zenhttp/servers/httpasio.cpp
+++ b/src/zenhttp/servers/httpasio.cpp
@@ -7,6 +7,7 @@
#include <zencore/fmtutils.h>
#include <zencore/logging.h>
#include <zencore/memory/llm.h>
+#include <zencore/system.h>
#include <zencore/thread.h>
#include <zencore/trace.h>
#include <zencore/windows.h>
@@ -89,10 +90,10 @@ IsIPv6AvailableSysctl(void)
char buf[16];
if (fgets(buf, sizeof(buf), f))
{
- fclose(f);
// 0 means IPv6 enabled, 1 means disabled
val = atoi(buf);
}
+ fclose(f);
}
return val == 0;
@@ -506,6 +507,8 @@ public:
HttpService* RouteRequest(std::string_view Url);
IHttpRequestFilter::Result FilterRequest(HttpServerRequest& Request);
+ bool IsLoopbackOnly() const;
+
asio::io_service m_IoService;
asio::io_service::work m_Work{m_IoService};
std::unique_ptr<asio_http::HttpAcceptor> m_Acceptor;
@@ -1544,7 +1547,7 @@ struct HttpAcceptor
{
ZEN_WARN("Unable to initialize asio service, (bind returned '{}')", BindErrorCode.message());
- return 0;
+ return {};
}
if (EffectivePort != BasePort)
@@ -1601,7 +1604,8 @@ struct HttpAcceptor
void StopAccepting() { m_IsStopped = true; }
- int GetAcceptPort() { return m_Acceptor.local_endpoint().port(); }
+ int GetAcceptPort() const { return m_Acceptor.local_endpoint().port(); }
+ bool IsLoopbackOnly() const { return m_Acceptor.local_endpoint().address().is_loopback(); }
bool IsValid() const { return m_IsValid; }
@@ -1975,6 +1979,12 @@ HttpAsioServerImpl::FilterRequest(HttpServerRequest& Request)
return RequestFilter->FilterRequest(Request);
}
+bool
+HttpAsioServerImpl::IsLoopbackOnly() const
+{
+ return m_Acceptor && m_Acceptor->IsLoopbackOnly();
+}
+
} // namespace zen::asio_http
//////////////////////////////////////////////////////////////////////////
@@ -1987,12 +1997,13 @@ public:
HttpAsioServer(const AsioConfig& Config);
~HttpAsioServer();
- virtual void OnRegisterService(HttpService& Service) override;
- virtual int OnInitialize(int BasePort, std::filesystem::path DataDir) override;
- virtual void OnSetHttpRequestFilter(IHttpRequestFilter* RequestFilter) override;
- virtual void OnRun(bool IsInteractiveSession) override;
- virtual void OnRequestExit() override;
- virtual void OnClose() override;
+ virtual void OnRegisterService(HttpService& Service) override;
+ virtual int OnInitialize(int BasePort, std::filesystem::path DataDir) override;
+ virtual void OnSetHttpRequestFilter(IHttpRequestFilter* RequestFilter) override;
+ virtual void OnRun(bool IsInteractiveSession) override;
+ virtual void OnRequestExit() override;
+ virtual void OnClose() override;
+ virtual std::string OnGetExternalHost() const override;
private:
Event m_ShutdownEvent;
@@ -2067,6 +2078,29 @@ HttpAsioServer::OnInitialize(int BasePort, std::filesystem::path DataDir)
return m_BasePort;
}
+std::string
+HttpAsioServer::OnGetExternalHost() const
+{
+ if (m_Impl->IsLoopbackOnly())
+ {
+ return "127.0.0.1";
+ }
+
+ // Use the UDP connect trick: connecting a UDP socket to an external address
+ // causes the OS to select the appropriate local interface without sending any data.
+ try
+ {
+ asio::io_service IoService;
+ asio::ip::udp::socket Sock(IoService, asio::ip::udp::v4());
+ Sock.connect(asio::ip::udp::endpoint(asio::ip::address::from_string("8.8.8.8"), 80));
+ return Sock.local_endpoint().address().to_string();
+ }
+ catch (const std::exception&)
+ {
+ return GetMachineName();
+ }
+}
+
void
HttpAsioServer::OnRun(bool IsInteractive)
{
diff --git a/src/zenhttp/servers/httpmulti.cpp b/src/zenhttp/servers/httpmulti.cpp
index 310ac9dc0..584e06cbf 100644
--- a/src/zenhttp/servers/httpmulti.cpp
+++ b/src/zenhttp/servers/httpmulti.cpp
@@ -117,6 +117,16 @@ HttpMultiServer::OnClose()
}
}
+std::string
+HttpMultiServer::OnGetExternalHost() const
+{
+ if (!m_Servers.empty())
+ {
+ return std::string(m_Servers.front()->GetExternalHost());
+ }
+ return HttpServer::OnGetExternalHost();
+}
+
void
HttpMultiServer::AddServer(Ref<HttpServer> Server)
{
diff --git a/src/zenhttp/servers/httpmulti.h b/src/zenhttp/servers/httpmulti.h
index 1897587a9..97699828a 100644
--- a/src/zenhttp/servers/httpmulti.h
+++ b/src/zenhttp/servers/httpmulti.h
@@ -15,12 +15,13 @@ public:
HttpMultiServer();
~HttpMultiServer();
- virtual void OnRegisterService(HttpService& Service) override;
- virtual void OnSetHttpRequestFilter(IHttpRequestFilter* RequestFilter) override;
- virtual int OnInitialize(int BasePort, std::filesystem::path DataDir) override;
- virtual void OnRun(bool IsInteractiveSession) override;
- virtual void OnRequestExit() override;
- virtual void OnClose() override;
+ virtual void OnRegisterService(HttpService& Service) override;
+ virtual void OnSetHttpRequestFilter(IHttpRequestFilter* RequestFilter) override;
+ virtual int OnInitialize(int BasePort, std::filesystem::path DataDir) override;
+ virtual void OnRun(bool IsInteractiveSession) override;
+ virtual void OnRequestExit() override;
+ virtual void OnClose() override;
+ virtual std::string OnGetExternalHost() const override;
void AddServer(Ref<HttpServer> Server);
diff --git a/src/zenhttp/servers/httpparser.cpp b/src/zenhttp/servers/httpparser.cpp
index be5befcd2..f0485aa25 100644
--- a/src/zenhttp/servers/httpparser.cpp
+++ b/src/zenhttp/servers/httpparser.cpp
@@ -226,6 +226,8 @@ NormalizeUrlPath(std::string_view InUrl, std::string& NormalizedUrl)
NormalizedUrl.append(Url, UrlIndex);
}
+ // NOTE: this check is redundant given the enclosing if,
+ // need to verify the intent of this code
if (!LastCharWasSeparator)
{
NormalizedUrl.push_back('/');
@@ -310,6 +312,7 @@ HttpRequestParser::OnHeadersComplete()
if (ContentLength)
{
+ // TODO: should sanity-check content length here
m_BodyBuffer = IoBuffer(ContentLength);
}
@@ -329,9 +332,9 @@ HttpRequestParser::OnHeadersComplete()
int
HttpRequestParser::OnBody(const char* Data, size_t Bytes)
{
- if (m_BodyPosition + Bytes > m_BodyBuffer.Size())
+ if ((m_BodyPosition + Bytes) > m_BodyBuffer.Size())
{
- ZEN_WARN("HTTP parser incoming body is larger than content size, need {} more bytes",
+ ZEN_WARN("HTTP parser incoming body is larger than content size, need {} more buffer bytes",
(m_BodyPosition + Bytes) - m_BodyBuffer.Size());
return 1;
}
@@ -342,7 +345,7 @@ HttpRequestParser::OnBody(const char* Data, size_t Bytes)
{
if (m_BodyPosition != m_BodyBuffer.Size())
{
- ZEN_WARN("Body mismatch! {} != {}", m_BodyPosition, m_BodyBuffer.Size());
+ ZEN_WARN("Body size mismatch! {} != {}", m_BodyPosition, m_BodyBuffer.Size());
return 1;
}
}
diff --git a/src/zenhttp/servers/httpsys.cpp b/src/zenhttp/servers/httpsys.cpp
index c640ba90b..e93ae4853 100644
--- a/src/zenhttp/servers/httpsys.cpp
+++ b/src/zenhttp/servers/httpsys.cpp
@@ -12,6 +12,7 @@
#include <zencore/memory/llm.h>
#include <zencore/scopeguard.h>
#include <zencore/string.h>
+#include <zencore/system.h>
#include <zencore/timer.h>
#include <zencore/trace.h>
#include <zenhttp/packageformat.h>
@@ -25,7 +26,9 @@
# include <zencore/workthreadpool.h>
# include "iothreadpool.h"
+# include <atomic>
# include <http.h>
+# include <asio.hpp> // for resolving addresses for GetExternalHost
namespace zen {
@@ -92,12 +95,13 @@ public:
// HttpServer interface implementation
- virtual int OnInitialize(int BasePort, std::filesystem::path DataDir) override;
- virtual void OnRun(bool TestMode) override;
- virtual void OnRequestExit() override;
- virtual void OnRegisterService(HttpService& Service) override;
- virtual void OnSetHttpRequestFilter(IHttpRequestFilter* RequestFilter) override;
- virtual void OnClose() override;
+ virtual int OnInitialize(int BasePort, std::filesystem::path DataDir) override;
+ virtual void OnRun(bool TestMode) override;
+ virtual void OnRequestExit() override;
+ virtual void OnRegisterService(HttpService& Service) override;
+ virtual void OnSetHttpRequestFilter(IHttpRequestFilter* RequestFilter) override;
+ virtual void OnClose() override;
+ virtual std::string OnGetExternalHost() const override;
WorkerThreadPool& WorkPool();
@@ -129,8 +133,8 @@ private:
std::unique_ptr<WinIoThreadPool> m_IoThreadPool;
- RwLock m_AsyncWorkPoolInitLock;
- WorkerThreadPool* m_AsyncWorkPool = nullptr;
+ RwLock m_AsyncWorkPoolInitLock;
+ std::atomic<WorkerThreadPool*> m_AsyncWorkPool = nullptr;
std::vector<std::wstring> m_BaseUris; // eg: http://*:nnnn/
HTTP_SERVER_SESSION_ID m_HttpSessionId = 0;
@@ -1032,8 +1036,10 @@ HttpSysServer::~HttpSysServer()
ZEN_ERROR("~HttpSysServer() called without calling Close() first");
}
- delete m_AsyncWorkPool;
+ auto WorkPool = m_AsyncWorkPool.load(std::memory_order_relaxed);
m_AsyncWorkPool = nullptr;
+
+ delete WorkPool;
}
void
@@ -1323,17 +1329,17 @@ HttpSysServer::WorkPool()
{
ZEN_MEMSCOPE(GetHttpsysTag());
- if (!m_AsyncWorkPool)
+ if (!m_AsyncWorkPool.load(std::memory_order_acquire))
{
RwLock::ExclusiveLockScope _(m_AsyncWorkPoolInitLock);
- if (!m_AsyncWorkPool)
+ if (!m_AsyncWorkPool.load(std::memory_order_relaxed))
{
- m_AsyncWorkPool = new WorkerThreadPool(m_InitialConfig.AsyncWorkThreadCount, "http_async");
+ m_AsyncWorkPool.store(new WorkerThreadPool(m_InitialConfig.AsyncWorkThreadCount, "http_async"), std::memory_order_release);
}
}
- return *m_AsyncWorkPool;
+ return *m_AsyncWorkPool.load(std::memory_order_relaxed);
}
void
@@ -2287,6 +2293,40 @@ HttpSysServer::OnRequestExit()
m_ShutdownEvent.Set();
}
+std::string
+HttpSysServer::OnGetExternalHost() const
+{
+ // Check whether we registered a public wildcard URL (http://*:port/) or fell back to loopback
+ bool IsPublic = false;
+ for (const auto& Uri : m_BaseUris)
+ {
+ if (Uri.find(L'*') != std::wstring::npos)
+ {
+ IsPublic = true;
+ break;
+ }
+ }
+
+ if (!IsPublic)
+ {
+ return "127.0.0.1";
+ }
+
+ // Use the UDP connect trick: connecting a UDP socket to an external address
+ // causes the OS to select the appropriate local interface without sending any data.
+ try
+ {
+ asio::io_service IoService;
+ asio::ip::udp::socket Sock(IoService, asio::ip::udp::v4());
+ Sock.connect(asio::ip::udp::endpoint(asio::ip::address::from_string("8.8.8.8"), 80));
+ return Sock.local_endpoint().address().to_string();
+ }
+ catch (const std::exception&)
+ {
+ return GetMachineName();
+ }
+}
+
void
HttpSysServer::OnRegisterService(HttpService& Service)
{
diff --git a/src/zenremotestore/builds/buildstoragecache.cpp b/src/zenremotestore/builds/buildstoragecache.cpp
index 07fcd62ba..faa85f81b 100644
--- a/src/zenremotestore/builds/buildstoragecache.cpp
+++ b/src/zenremotestore/builds/buildstoragecache.cpp
@@ -474,7 +474,13 @@ TestZenCacheEndpoint(std::string_view BaseUrl, const bool AssumeHttp2, const boo
HttpClient::Response TestResponse = TestHttpClient.Get("/status/builds");
if (TestResponse.IsSuccess())
{
- return {.Success = true};
+ LatencyTestResult LatencyResult = MeasureLatency(TestHttpClient, "/health");
+
+ if (!LatencyResult.Success)
+ {
+ return {.Success = false, .FailureReason = LatencyResult.FailureReason};
+ }
+ return {.Success = true, .LatencySeconds = LatencyResult.LatencySeconds};
}
return {.Success = false, .FailureReason = TestResponse.ErrorMessage("")};
};
diff --git a/src/zenremotestore/builds/buildstorageoperations.cpp b/src/zenremotestore/builds/buildstorageoperations.cpp
index ade431393..5219e86d8 100644
--- a/src/zenremotestore/builds/buildstorageoperations.cpp
+++ b/src/zenremotestore/builds/buildstorageoperations.cpp
@@ -484,24 +484,6 @@ private:
uint64_t FilteredPerSecond = 0;
};
-EPartialBlockRequestMode
-PartialBlockRequestModeFromString(const std::string_view ModeString)
-{
- switch (HashStringAsLowerDjb2(ModeString))
- {
- case HashStringDjb2("false"):
- return EPartialBlockRequestMode::Off;
- case HashStringDjb2("zencacheonly"):
- return EPartialBlockRequestMode::ZenCacheOnly;
- case HashStringDjb2("mixed"):
- return EPartialBlockRequestMode::Mixed;
- case HashStringDjb2("true"):
- return EPartialBlockRequestMode::All;
- default:
- return EPartialBlockRequestMode::Invalid;
- }
-}
-
std::filesystem::path
ZenStateFilePath(const std::filesystem::path& ZenFolderPath)
{
@@ -579,13 +561,6 @@ BuildsOperationUpdateFolder::Execute(FolderContent& OutLocalFolderState)
CreateDirectories(m_TempDownloadFolderPath);
CreateDirectories(m_TempBlockFolderPath);
- Stopwatch IndexTimer;
-
- if (!m_Options.IsQuiet)
- {
- ZEN_OPERATION_LOG_INFO(m_LogOutput, "Indexed local and remote content in {}", NiceTimeSpanMs(IndexTimer.GetElapsedTimeMs()));
- }
-
Stopwatch CacheMappingTimer;
std::vector<std::atomic<uint32_t>> SequenceIndexChunksLeftToWriteCounters(m_RemoteContent.ChunkedContent.SequenceRawHashes.size());
@@ -906,343 +881,220 @@ BuildsOperationUpdateFolder::Execute(FolderContent& OutLocalFolderState)
CheckRequiredDiskSpace(RemotePathToRemoteIndex);
+ BlobsExistsResult ExistsResult;
{
- ZEN_TRACE_CPU("WriteChunks");
-
- m_LogOutput.SetLogOperationProgress((uint32_t)TaskSteps::WriteChunks, (uint32_t)TaskSteps::StepCount);
+ ChunkBlockAnalyser BlockAnalyser(m_LogOutput,
+ m_BlockDescriptions,
+ ChunkBlockAnalyser::Options{.IsQuiet = m_Options.IsQuiet,
+ .IsVerbose = m_Options.IsVerbose,
+ .HostLatencySec = m_Storage.BuildStorageLatencySec,
+ .HostHighSpeedLatencySec = m_Storage.CacheLatencySec});
- Stopwatch WriteTimer;
+ std::vector<ChunkBlockAnalyser::NeededBlock> NeededBlocks = BlockAnalyser.GetNeeded(
+ m_RemoteLookup.ChunkHashToChunkIndex,
+ [&](uint32_t RemoteChunkIndex) -> bool { return RemoteChunkIndexNeedsCopyFromSourceFlags[RemoteChunkIndex]; });
- FilteredRate FilteredDownloadedBytesPerSecond;
- FilteredRate FilteredWrittenBytesPerSecond;
+ std::vector<uint32_t> FetchBlockIndexes;
+ std::vector<uint32_t> CachedChunkBlockIndexes;
- std::unique_ptr<OperationLogOutput::ProgressBar> WriteProgressBarPtr(
- m_LogOutput.CreateProgressBar(m_Options.PrimeCacheOnly ? "Downloading" : "Writing"));
- OperationLogOutput::ProgressBar& WriteProgressBar(*WriteProgressBarPtr);
- ParallelWork Work(m_AbortFlag, m_PauseFlag, WorkerThreadPool::EMode::EnableBacklog);
-
- struct LooseChunkHashWorkData
{
- std::vector<const ChunkedContentLookup::ChunkSequenceLocation*> ChunkTargetPtrs;
- uint32_t RemoteChunkIndex = (uint32_t)-1;
- };
-
- std::vector<LooseChunkHashWorkData> LooseChunkHashWorks;
- TotalPartWriteCount += CopyChunkDatas.size();
- TotalPartWriteCount += ScavengedSequenceCopyOperations.size();
-
- for (const IoHash ChunkHash : m_LooseChunkHashes)
- {
- auto RemoteChunkIndexIt = m_RemoteLookup.ChunkHashToChunkIndex.find(ChunkHash);
- ZEN_ASSERT(RemoteChunkIndexIt != m_RemoteLookup.ChunkHashToChunkIndex.end());
- const uint32_t RemoteChunkIndex = RemoteChunkIndexIt->second;
- if (RemoteChunkIndexNeedsCopyFromLocalFileFlags[RemoteChunkIndex])
+ ZEN_TRACE_CPU("BlockCacheFileExists");
+ for (const ChunkBlockAnalyser::NeededBlock& NeededBlock : NeededBlocks)
{
- if (m_Options.IsVerbose)
- {
- ZEN_OPERATION_LOG_INFO(m_LogOutput, "Skipping chunk {} due to cache reuse", ChunkHash);
- }
- continue;
- }
- bool NeedsCopy = true;
- if (RemoteChunkIndexNeedsCopyFromSourceFlags[RemoteChunkIndex].compare_exchange_strong(NeedsCopy, false))
- {
- std::vector<const ChunkedContentLookup::ChunkSequenceLocation*> ChunkTargetPtrs =
- GetRemainingChunkTargets(SequenceIndexChunksLeftToWriteCounters, RemoteChunkIndex);
-
- if (ChunkTargetPtrs.empty())
+ if (m_Options.PrimeCacheOnly)
{
- if (m_Options.IsVerbose)
- {
- ZEN_OPERATION_LOG_INFO(m_LogOutput, "Skipping chunk {} due to cache reuse", ChunkHash);
- }
+ FetchBlockIndexes.push_back(NeededBlock.BlockIndex);
}
else
{
- TotalRequestCount++;
- TotalPartWriteCount++;
- LooseChunkHashWorks.push_back(
- LooseChunkHashWorkData{.ChunkTargetPtrs = ChunkTargetPtrs, .RemoteChunkIndex = RemoteChunkIndex});
- }
- }
- }
-
- uint32_t BlockCount = gsl::narrow<uint32_t>(m_BlockDescriptions.size());
-
- std::vector<bool> ChunkIsPickedUpByBlock(m_RemoteContent.ChunkedContent.ChunkHashes.size(), false);
- auto GetNeededChunkBlockIndexes = [this, &RemoteChunkIndexNeedsCopyFromSourceFlags, &ChunkIsPickedUpByBlock](
- const ChunkBlockDescription& BlockDescription) {
- ZEN_TRACE_CPU("GetNeededChunkBlockIndexes");
- std::vector<uint32_t> NeededBlockChunkIndexes;
- for (uint32_t ChunkBlockIndex = 0; ChunkBlockIndex < BlockDescription.ChunkRawHashes.size(); ChunkBlockIndex++)
- {
- const IoHash& ChunkHash = BlockDescription.ChunkRawHashes[ChunkBlockIndex];
- if (auto It = m_RemoteLookup.ChunkHashToChunkIndex.find(ChunkHash); It != m_RemoteLookup.ChunkHashToChunkIndex.end())
- {
- const uint32_t RemoteChunkIndex = It->second;
- if (!ChunkIsPickedUpByBlock[RemoteChunkIndex])
+ const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[NeededBlock.BlockIndex];
+ bool UsingCachedBlock = false;
+ if (auto It = CachedBlocksFound.find(BlockDescription.BlockHash); It != CachedBlocksFound.end())
{
- if (RemoteChunkIndexNeedsCopyFromSourceFlags[RemoteChunkIndex])
+ TotalPartWriteCount++;
+
+ std::filesystem::path BlockPath = m_TempBlockFolderPath / BlockDescription.BlockHash.ToHexString();
+ if (IsFile(BlockPath))
{
- ChunkIsPickedUpByBlock[RemoteChunkIndex] = true;
- NeededBlockChunkIndexes.push_back(ChunkBlockIndex);
+ CachedChunkBlockIndexes.push_back(NeededBlock.BlockIndex);
+ UsingCachedBlock = true;
}
}
- }
- else
- {
- ZEN_DEBUG("Chunk {} not found in block {}", ChunkHash, BlockDescription.BlockHash);
+ if (!UsingCachedBlock)
+ {
+ FetchBlockIndexes.push_back(NeededBlock.BlockIndex);
+ }
}
}
- return NeededBlockChunkIndexes;
- };
+ }
- std::vector<uint32_t> CachedChunkBlockIndexes;
- std::vector<uint32_t> FetchBlockIndexes;
- std::vector<std::vector<uint32_t>> AllBlockChunkIndexNeeded;
+ std::vector<uint32_t> NeededLooseChunkIndexes;
- for (uint32_t BlockIndex = 0; BlockIndex < BlockCount; BlockIndex++)
{
- const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex];
-
- std::vector<uint32_t> BlockChunkIndexNeeded = GetNeededChunkBlockIndexes(BlockDescription);
- if (!BlockChunkIndexNeeded.empty())
+ NeededLooseChunkIndexes.reserve(m_LooseChunkHashes.size());
+ for (uint32_t LooseChunkIndex = 0; LooseChunkIndex < m_LooseChunkHashes.size(); LooseChunkIndex++)
{
- if (m_Options.PrimeCacheOnly)
+ const IoHash& ChunkHash = m_LooseChunkHashes[LooseChunkIndex];
+ auto RemoteChunkIndexIt = m_RemoteLookup.ChunkHashToChunkIndex.find(ChunkHash);
+ ZEN_ASSERT(RemoteChunkIndexIt != m_RemoteLookup.ChunkHashToChunkIndex.end());
+ const uint32_t RemoteChunkIndex = RemoteChunkIndexIt->second;
+
+ if (RemoteChunkIndexNeedsCopyFromLocalFileFlags[RemoteChunkIndex])
{
- FetchBlockIndexes.push_back(BlockIndex);
+ if (m_Options.IsVerbose)
+ {
+ ZEN_OPERATION_LOG_INFO(m_LogOutput,
+ "Skipping chunk {} due to cache reuse",
+ m_RemoteContent.ChunkedContent.ChunkHashes[RemoteChunkIndex]);
+ }
+ continue;
}
- else
+
+ bool NeedsCopy = true;
+ if (RemoteChunkIndexNeedsCopyFromSourceFlags[RemoteChunkIndex].compare_exchange_strong(NeedsCopy, false))
{
- bool UsingCachedBlock = false;
- if (auto It = CachedBlocksFound.find(BlockDescription.BlockHash); It != CachedBlocksFound.end())
+ uint64_t WriteCount = GetChunkWriteCount(SequenceIndexChunksLeftToWriteCounters, RemoteChunkIndex);
+ if (WriteCount == 0)
{
- TotalPartWriteCount++;
-
- std::filesystem::path BlockPath = m_TempBlockFolderPath / BlockDescription.BlockHash.ToHexString();
- if (IsFile(BlockPath))
+ if (m_Options.IsVerbose)
{
- CachedChunkBlockIndexes.push_back(BlockIndex);
- UsingCachedBlock = true;
+ ZEN_OPERATION_LOG_INFO(m_LogOutput,
+ "Skipping chunk {} due to cache reuse",
+ m_RemoteContent.ChunkedContent.ChunkHashes[RemoteChunkIndex]);
}
}
- if (!UsingCachedBlock)
+ else
{
- FetchBlockIndexes.push_back(BlockIndex);
+ NeededLooseChunkIndexes.push_back(LooseChunkIndex);
}
}
}
- AllBlockChunkIndexNeeded.emplace_back(std::move(BlockChunkIndexNeeded));
}
- BlobsExistsResult ExistsResult;
-
if (m_Storage.BuildCacheStorage)
{
ZEN_TRACE_CPU("BlobCacheExistCheck");
Stopwatch Timer;
- tsl::robin_set<IoHash> BlobHashesSet;
+ std::vector<IoHash> BlobHashes;
+ BlobHashes.reserve(NeededLooseChunkIndexes.size() + FetchBlockIndexes.size());
- BlobHashesSet.reserve(LooseChunkHashWorks.size() + FetchBlockIndexes.size());
- for (LooseChunkHashWorkData& LooseChunkHashWork : LooseChunkHashWorks)
+ for (const uint32_t LooseChunkIndex : NeededLooseChunkIndexes)
{
- BlobHashesSet.insert(m_RemoteContent.ChunkedContent.ChunkHashes[LooseChunkHashWork.RemoteChunkIndex]);
+ BlobHashes.push_back(m_LooseChunkHashes[LooseChunkIndex]);
}
+
for (uint32_t BlockIndex : FetchBlockIndexes)
{
- const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex];
- BlobHashesSet.insert(BlockDescription.BlockHash);
+ BlobHashes.push_back(m_BlockDescriptions[BlockIndex].BlockHash);
}
- if (!BlobHashesSet.empty())
- {
- const std::vector<IoHash> BlobHashes(BlobHashesSet.begin(), BlobHashesSet.end());
- const std::vector<BuildStorageCache::BlobExistsResult> CacheExistsResult =
- m_Storage.BuildCacheStorage->BlobsExists(m_BuildId, BlobHashes);
+ const std::vector<BuildStorageCache::BlobExistsResult> CacheExistsResult =
+ m_Storage.BuildCacheStorage->BlobsExists(m_BuildId, BlobHashes);
- if (CacheExistsResult.size() == BlobHashes.size())
+ if (CacheExistsResult.size() == BlobHashes.size())
+ {
+ ExistsResult.ExistingBlobs.reserve(CacheExistsResult.size());
+ for (size_t BlobIndex = 0; BlobIndex < BlobHashes.size(); BlobIndex++)
{
- ExistsResult.ExistingBlobs.reserve(CacheExistsResult.size());
- for (size_t BlobIndex = 0; BlobIndex < BlobHashes.size(); BlobIndex++)
+ if (CacheExistsResult[BlobIndex].HasBody)
{
- if (CacheExistsResult[BlobIndex].HasBody)
- {
- ExistsResult.ExistingBlobs.insert(BlobHashes[BlobIndex]);
- }
+ ExistsResult.ExistingBlobs.insert(BlobHashes[BlobIndex]);
}
}
- ExistsResult.ElapsedTimeMs = Timer.GetElapsedTimeMs();
- if (!ExistsResult.ExistingBlobs.empty() && !m_Options.IsQuiet)
- {
- ZEN_OPERATION_LOG_INFO(m_LogOutput,
- "Remote cache : Found {} out of {} needed blobs in {}",
- ExistsResult.ExistingBlobs.size(),
- BlobHashes.size(),
- NiceTimeSpanMs(ExistsResult.ElapsedTimeMs));
- }
+ }
+ ExistsResult.ElapsedTimeMs = Timer.GetElapsedTimeMs();
+ if (!ExistsResult.ExistingBlobs.empty() && !m_Options.IsQuiet)
+ {
+ ZEN_OPERATION_LOG_INFO(m_LogOutput,
+ "Remote cache : Found {} out of {} needed blobs in {}",
+ ExistsResult.ExistingBlobs.size(),
+ BlobHashes.size(),
+ NiceTimeSpanMs(ExistsResult.ElapsedTimeMs));
}
}
- std::vector<BlockRangeDescriptor> BlockRangeWorks;
- std::vector<uint32_t> FullBlockWorks;
+ std::vector<ChunkBlockAnalyser::EPartialBlockDownloadMode> BlockPartialDownloadModes;
+ if (m_Options.PartialBlockRequestMode == EPartialBlockRequestMode::Off)
{
- Stopwatch Timer;
-
- std::vector<uint32_t> PartialBlockIndexes;
-
- for (uint32_t BlockIndex : FetchBlockIndexes)
+ BlockPartialDownloadModes.resize(m_BlockDescriptions.size(), ChunkBlockAnalyser::EPartialBlockDownloadMode::Off);
+ }
+ else
+ {
+ BlockPartialDownloadModes.reserve(m_BlockDescriptions.size());
+ for (uint32_t BlockIndex = 0; BlockIndex < m_BlockDescriptions.size(); BlockIndex++)
{
- const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex];
-
- const std::vector<uint32_t> BlockChunkIndexNeeded = std::move(AllBlockChunkIndexNeeded[BlockIndex]);
- if (!BlockChunkIndexNeeded.empty())
+ const bool BlockExistInCache = ExistsResult.ExistingBlobs.contains(m_BlockDescriptions[BlockIndex].BlockHash);
+ if (m_Options.PartialBlockRequestMode == EPartialBlockRequestMode::All)
{
- bool WantsToDoPartialBlockDownload = BlockChunkIndexNeeded.size() < BlockDescription.ChunkRawHashes.size();
- bool CanDoPartialBlockDownload =
- (BlockDescription.HeaderSize > 0) &&
- (BlockDescription.ChunkCompressedLengths.size() == BlockDescription.ChunkRawHashes.size());
-
- bool AllowedToDoPartialRequest = false;
- bool BlockExistInCache = ExistsResult.ExistingBlobs.contains(BlockDescription.BlockHash);
- switch (m_Options.PartialBlockRequestMode)
- {
- case EPartialBlockRequestMode::Off:
- break;
- case EPartialBlockRequestMode::ZenCacheOnly:
- AllowedToDoPartialRequest = BlockExistInCache;
- break;
- case EPartialBlockRequestMode::Mixed:
- case EPartialBlockRequestMode::All:
- AllowedToDoPartialRequest = true;
- break;
- default:
- ZEN_ASSERT(false);
- break;
- }
+ BlockPartialDownloadModes.push_back(BlockExistInCache
+ ? ChunkBlockAnalyser::EPartialBlockDownloadMode::MultiRangeHighSpeed
+ : ChunkBlockAnalyser::EPartialBlockDownloadMode::MultiRange);
+ }
+ else if (m_Options.PartialBlockRequestMode == EPartialBlockRequestMode::ZenCacheOnly)
+ {
+ BlockPartialDownloadModes.push_back(BlockExistInCache
+ ? ChunkBlockAnalyser::EPartialBlockDownloadMode::MultiRangeHighSpeed
+ : ChunkBlockAnalyser::EPartialBlockDownloadMode::Off);
+ }
+ else if (m_Options.PartialBlockRequestMode == EPartialBlockRequestMode::Mixed)
+ {
+ BlockPartialDownloadModes.push_back(BlockExistInCache
+ ? ChunkBlockAnalyser::EPartialBlockDownloadMode::MultiRangeHighSpeed
+ : ChunkBlockAnalyser::EPartialBlockDownloadMode::SingleRange);
+ }
+ }
+ }
+ ZEN_ASSERT(BlockPartialDownloadModes.size() == m_BlockDescriptions.size());
- const uint32_t ChunkStartOffsetInBlock =
- gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize);
+ ChunkBlockAnalyser::BlockResult PartialBlocks =
+ BlockAnalyser.CalculatePartialBlockDownloads(NeededBlocks, BlockPartialDownloadModes);
- const uint64_t TotalBlockSize = std::accumulate(BlockDescription.ChunkCompressedLengths.begin(),
- BlockDescription.ChunkCompressedLengths.end(),
- std::uint64_t(ChunkStartOffsetInBlock));
+ struct LooseChunkHashWorkData
+ {
+ std::vector<const ChunkedContentLookup::ChunkSequenceLocation*> ChunkTargetPtrs;
+ uint32_t RemoteChunkIndex = (uint32_t)-1;
+ };
- if (AllowedToDoPartialRequest && WantsToDoPartialBlockDownload && CanDoPartialBlockDownload)
- {
- ZEN_TRACE_CPU("PartialBlockAnalysis");
-
- bool LimitToSingleRange =
- BlockExistInCache ? false : m_Options.PartialBlockRequestMode == EPartialBlockRequestMode::Mixed;
- uint64_t TotalWantedChunksSize = 0;
- std::optional<std::vector<BlockRangeDescriptor>> MaybeBlockRanges =
- CalculateBlockRanges(BlockIndex,
- BlockDescription,
- BlockChunkIndexNeeded,
- LimitToSingleRange,
- ChunkStartOffsetInBlock,
- TotalBlockSize,
- TotalWantedChunksSize);
- ZEN_ASSERT(TotalWantedChunksSize <= TotalBlockSize);
-
- if (MaybeBlockRanges.has_value())
- {
- const std::vector<BlockRangeDescriptor>& BlockRanges = MaybeBlockRanges.value();
- ZEN_ASSERT(!BlockRanges.empty());
- BlockRangeWorks.insert(BlockRangeWorks.end(), BlockRanges.begin(), BlockRanges.end());
- TotalRequestCount += BlockRanges.size();
- TotalPartWriteCount += BlockRanges.size();
-
- uint64_t RequestedSize = std::accumulate(
- BlockRanges.begin(),
- BlockRanges.end(),
- uint64_t(0),
- [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; });
- PartialBlockIndexes.push_back(BlockIndex);
-
- if (RequestedSize > TotalWantedChunksSize)
- {
- if (m_Options.IsVerbose)
- {
- ZEN_OPERATION_LOG_INFO(
- m_LogOutput,
- "Requesting {} chunks ({}) from block {} ({}) using {} requests (extra bytes {})",
- BlockChunkIndexNeeded.size(),
- NiceBytes(RequestedSize),
- BlockDescription.BlockHash,
- NiceBytes(TotalBlockSize),
- BlockRanges.size(),
- NiceBytes(RequestedSize - TotalWantedChunksSize));
- }
- }
- }
- else
- {
- FullBlockWorks.push_back(BlockIndex);
- TotalRequestCount++;
- TotalPartWriteCount++;
- }
- }
- else
- {
- FullBlockWorks.push_back(BlockIndex);
- TotalRequestCount++;
- TotalPartWriteCount++;
- }
- }
- }
+ TotalRequestCount += NeededLooseChunkIndexes.size();
+ TotalPartWriteCount += NeededLooseChunkIndexes.size();
+ TotalRequestCount += PartialBlocks.BlockRanges.size();
+ TotalPartWriteCount += PartialBlocks.BlockRanges.size();
+ TotalRequestCount += PartialBlocks.FullBlockIndexes.size();
+ TotalPartWriteCount += PartialBlocks.FullBlockIndexes.size();
- if (!PartialBlockIndexes.empty())
- {
- uint64_t TotalFullBlockRequestBytes = 0;
- for (uint32_t BlockIndex : FullBlockWorks)
- {
- const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex];
- uint32_t CurrentOffset =
- gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize);
+ std::vector<LooseChunkHashWorkData> LooseChunkHashWorks;
+ for (uint32_t LooseChunkIndex : NeededLooseChunkIndexes)
+ {
+ const IoHash& ChunkHash = m_LooseChunkHashes[LooseChunkIndex];
+ auto RemoteChunkIndexIt = m_RemoteLookup.ChunkHashToChunkIndex.find(ChunkHash);
+ ZEN_ASSERT(RemoteChunkIndexIt != m_RemoteLookup.ChunkHashToChunkIndex.end());
+ const uint32_t RemoteChunkIndex = RemoteChunkIndexIt->second;
- TotalFullBlockRequestBytes += std::accumulate(BlockDescription.ChunkCompressedLengths.begin(),
- BlockDescription.ChunkCompressedLengths.end(),
- std::uint64_t(CurrentOffset));
- }
+ std::vector<const ChunkedContentLookup::ChunkSequenceLocation*> ChunkTargetPtrs =
+ GetRemainingChunkTargets(SequenceIndexChunksLeftToWriteCounters, RemoteChunkIndex);
- uint64_t TotalPartialBlockBytes = 0;
- for (uint32_t BlockIndex : PartialBlockIndexes)
- {
- const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex];
- uint32_t CurrentOffset =
- gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize);
+ ZEN_ASSERT(!ChunkTargetPtrs.empty());
+ LooseChunkHashWorks.push_back(
+ LooseChunkHashWorkData{.ChunkTargetPtrs = ChunkTargetPtrs, .RemoteChunkIndex = RemoteChunkIndex});
+ }
- TotalPartialBlockBytes += std::accumulate(BlockDescription.ChunkCompressedLengths.begin(),
- BlockDescription.ChunkCompressedLengths.end(),
- std::uint64_t(CurrentOffset));
- }
+ ZEN_TRACE_CPU("WriteChunks");
- uint64_t NonPartialTotalBlockBytes = TotalFullBlockRequestBytes + TotalPartialBlockBytes;
+ m_LogOutput.SetLogOperationProgress((uint32_t)TaskSteps::WriteChunks, (uint32_t)TaskSteps::StepCount);
- const uint64_t TotalPartialBlockRequestBytes =
- std::accumulate(BlockRangeWorks.begin(),
- BlockRangeWorks.end(),
- uint64_t(0),
- [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; });
- uint64_t TotalExtraPartialBlocksRequests = BlockRangeWorks.size() - PartialBlockIndexes.size();
+ Stopwatch WriteTimer;
- uint64_t TotalSavedBlocksSize = TotalPartialBlockBytes - TotalPartialBlockRequestBytes;
- double SavedSizePercent = (TotalSavedBlocksSize * 100.0) / NonPartialTotalBlockBytes;
+ FilteredRate FilteredDownloadedBytesPerSecond;
+ FilteredRate FilteredWrittenBytesPerSecond;
- if (!m_Options.IsQuiet)
- {
- ZEN_OPERATION_LOG_INFO(m_LogOutput,
- "Analysis of partial block requests saves download of {} out of {} ({:.1f}%) using {} extra "
- "requests. Completed in {}",
- NiceBytes(TotalSavedBlocksSize),
- NiceBytes(NonPartialTotalBlockBytes),
- SavedSizePercent,
- TotalExtraPartialBlocksRequests,
- NiceTimeSpanMs(ExistsResult.ElapsedTimeMs));
- }
- }
- }
+ std::unique_ptr<OperationLogOutput::ProgressBar> WriteProgressBarPtr(
+ m_LogOutput.CreateProgressBar(m_Options.PrimeCacheOnly ? "Downloading" : "Writing"));
+ OperationLogOutput::ProgressBar& WriteProgressBar(*WriteProgressBarPtr);
+ ParallelWork Work(m_AbortFlag, m_PauseFlag, WorkerThreadPool::EMode::EnableBacklog);
+
+ TotalPartWriteCount += CopyChunkDatas.size();
+ TotalPartWriteCount += ScavengedSequenceCopyOperations.size();
BufferedWriteFileCache WriteCache;
@@ -1472,13 +1324,23 @@ BuildsOperationUpdateFolder::Execute(FolderContent& OutLocalFolderState)
});
}
- for (size_t BlockRangeIndex = 0; BlockRangeIndex < BlockRangeWorks.size(); BlockRangeIndex++)
+ for (size_t BlockRangeIndex = 0; BlockRangeIndex < PartialBlocks.BlockRanges.size();)
{
ZEN_ASSERT(!m_Options.PrimeCacheOnly);
if (m_AbortFlag)
{
break;
}
+
+ size_t RangeCount = 1;
+ size_t RangesLeft = PartialBlocks.BlockRanges.size() - BlockRangeIndex;
+ const ChunkBlockAnalyser::BlockRangeDescriptor& CurrentBlockRange = PartialBlocks.BlockRanges[BlockRangeIndex];
+ while (RangeCount < RangesLeft &&
+ CurrentBlockRange.BlockIndex == PartialBlocks.BlockRanges[BlockRangeIndex + RangeCount].BlockIndex)
+ {
+ RangeCount++;
+ }
+
Work.ScheduleWork(
m_NetworkPool,
[this,
@@ -1492,119 +1354,127 @@ BuildsOperationUpdateFolder::Execute(FolderContent& OutLocalFolderState)
TotalPartWriteCount,
&FilteredWrittenBytesPerSecond,
&Work,
- &BlockRangeWorks,
- BlockRangeIndex](std::atomic<bool>&) {
+ &PartialBlocks,
+ BlockRangeStartIndex = BlockRangeIndex,
+ RangeCount](std::atomic<bool>&) {
if (!m_AbortFlag)
{
- ZEN_TRACE_CPU("Async_GetPartialBlock");
-
- const BlockRangeDescriptor& BlockRange = BlockRangeWorks[BlockRangeIndex];
+ ZEN_TRACE_CPU("Async_GetPartialBlockRanges");
FilteredDownloadedBytesPerSecond.Start();
- DownloadPartialBlock(
- BlockRange,
- ExistsResult,
- [this,
- &RemoteChunkIndexNeedsCopyFromSourceFlags,
- &SequenceIndexChunksLeftToWriteCounters,
- &WritePartsComplete,
- &WriteCache,
- &Work,
- TotalRequestCount,
- TotalPartWriteCount,
- &FilteredDownloadedBytesPerSecond,
- &FilteredWrittenBytesPerSecond,
- &BlockRange](IoBuffer&& InMemoryBuffer, const std::filesystem::path& OnDiskPath) {
- if (m_DownloadStats.RequestsCompleteCount == TotalRequestCount)
- {
- FilteredDownloadedBytesPerSecond.Stop();
- }
-
- if (!m_AbortFlag)
- {
- Work.ScheduleWork(
- m_IOWorkerPool,
- [this,
- &RemoteChunkIndexNeedsCopyFromSourceFlags,
- &SequenceIndexChunksLeftToWriteCounters,
- &WritePartsComplete,
- &WriteCache,
- &Work,
- TotalPartWriteCount,
- &FilteredWrittenBytesPerSecond,
- &BlockRange,
- BlockChunkPath = std::filesystem::path(OnDiskPath),
- BlockPartialBuffer = std::move(InMemoryBuffer)](std::atomic<bool>&) mutable {
- if (!m_AbortFlag)
- {
- ZEN_TRACE_CPU("Async_WritePartialBlock");
+ for (size_t BlockRangeIndex = BlockRangeStartIndex; BlockRangeIndex < BlockRangeStartIndex + RangeCount;
+ BlockRangeIndex++)
+ {
+ ZEN_TRACE_CPU("GetPartialBlock");
- const uint32_t BlockIndex = BlockRange.BlockIndex;
+ const ChunkBlockAnalyser::BlockRangeDescriptor& BlockRange = PartialBlocks.BlockRanges[BlockRangeIndex];
- const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex];
+ DownloadPartialBlock(
+ BlockRange,
+ ExistsResult,
+ [this,
+ &RemoteChunkIndexNeedsCopyFromSourceFlags,
+ &SequenceIndexChunksLeftToWriteCounters,
+ &WritePartsComplete,
+ &WriteCache,
+ &Work,
+ TotalRequestCount,
+ TotalPartWriteCount,
+ &FilteredDownloadedBytesPerSecond,
+ &FilteredWrittenBytesPerSecond,
+ &BlockRange](IoBuffer&& InMemoryBuffer, const std::filesystem::path& OnDiskPath) {
+ if (m_DownloadStats.RequestsCompleteCount == TotalRequestCount)
+ {
+ FilteredDownloadedBytesPerSecond.Stop();
+ }
- if (BlockChunkPath.empty())
- {
- ZEN_ASSERT(BlockPartialBuffer);
- }
- else
+ if (!m_AbortFlag)
+ {
+ Work.ScheduleWork(
+ m_IOWorkerPool,
+ [this,
+ &RemoteChunkIndexNeedsCopyFromSourceFlags,
+ &SequenceIndexChunksLeftToWriteCounters,
+ &WritePartsComplete,
+ &WriteCache,
+ &Work,
+ TotalPartWriteCount,
+ &FilteredWrittenBytesPerSecond,
+ &BlockRange,
+ BlockChunkPath = std::filesystem::path(OnDiskPath),
+ BlockPartialBuffer = std::move(InMemoryBuffer)](std::atomic<bool>&) mutable {
+ if (!m_AbortFlag)
{
- ZEN_ASSERT(!BlockPartialBuffer);
- BlockPartialBuffer = IoBufferBuilder::MakeFromFile(BlockChunkPath);
- if (!BlockPartialBuffer)
+ ZEN_TRACE_CPU("Async_WritePartialBlock");
+
+ const uint32_t BlockIndex = BlockRange.BlockIndex;
+
+ const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex];
+
+ if (BlockChunkPath.empty())
{
- throw std::runtime_error(
- fmt::format("Could not open downloaded block {} from {}",
- BlockDescription.BlockHash,
- BlockChunkPath));
+ ZEN_ASSERT(BlockPartialBuffer);
+ }
+ else
+ {
+ ZEN_ASSERT(!BlockPartialBuffer);
+ BlockPartialBuffer = IoBufferBuilder::MakeFromFile(BlockChunkPath);
+ if (!BlockPartialBuffer)
+ {
+ throw std::runtime_error(
+ fmt::format("Could not open downloaded block {} from {}",
+ BlockDescription.BlockHash,
+ BlockChunkPath));
+ }
}
- }
-
- FilteredWrittenBytesPerSecond.Start();
- if (!WritePartialBlockChunksToCache(
- BlockDescription,
- SequenceIndexChunksLeftToWriteCounters,
- Work,
- CompositeBuffer(std::move(BlockPartialBuffer)),
- BlockRange.ChunkBlockIndexStart,
- BlockRange.ChunkBlockIndexStart + BlockRange.ChunkBlockIndexCount - 1,
- RemoteChunkIndexNeedsCopyFromSourceFlags,
- WriteCache))
- {
- std::error_code DummyEc;
- RemoveFile(BlockChunkPath, DummyEc);
- throw std::runtime_error(
- fmt::format("Partial block {} is malformed", BlockDescription.BlockHash));
- }
+ FilteredWrittenBytesPerSecond.Start();
+
+ if (!WritePartialBlockChunksToCache(
+ BlockDescription,
+ SequenceIndexChunksLeftToWriteCounters,
+ Work,
+ CompositeBuffer(std::move(BlockPartialBuffer)),
+ BlockRange.ChunkBlockIndexStart,
+ BlockRange.ChunkBlockIndexStart + BlockRange.ChunkBlockIndexCount - 1,
+ RemoteChunkIndexNeedsCopyFromSourceFlags,
+ WriteCache))
+ {
+ std::error_code DummyEc;
+ RemoveFile(BlockChunkPath, DummyEc);
+ throw std::runtime_error(
+ fmt::format("Partial block {} is malformed", BlockDescription.BlockHash));
+ }
- std::error_code Ec = TryRemoveFile(BlockChunkPath);
- if (Ec)
- {
- ZEN_OPERATION_LOG_DEBUG(m_LogOutput,
- "Failed removing file '{}', reason: ({}) {}",
- BlockChunkPath,
- Ec.value(),
- Ec.message());
- }
+ std::error_code Ec = TryRemoveFile(BlockChunkPath);
+ if (Ec)
+ {
+ ZEN_OPERATION_LOG_DEBUG(m_LogOutput,
+ "Failed removing file '{}', reason: ({}) {}",
+ BlockChunkPath,
+ Ec.value(),
+ Ec.message());
+ }
- WritePartsComplete++;
- if (WritePartsComplete == TotalPartWriteCount)
- {
- FilteredWrittenBytesPerSecond.Stop();
+ WritePartsComplete++;
+ if (WritePartsComplete == TotalPartWriteCount)
+ {
+ FilteredWrittenBytesPerSecond.Stop();
+ }
}
- }
- },
- OnDiskPath.empty() ? WorkerThreadPool::EMode::DisableBacklog
- : WorkerThreadPool::EMode::EnableBacklog);
- }
- });
+ },
+ OnDiskPath.empty() ? WorkerThreadPool::EMode::DisableBacklog
+ : WorkerThreadPool::EMode::EnableBacklog);
+ }
+ });
+ }
}
});
+ BlockRangeIndex += RangeCount;
}
- for (uint32_t BlockIndex : FullBlockWorks)
+ for (uint32_t BlockIndex : PartialBlocks.FullBlockIndexes)
{
if (m_AbortFlag)
{
@@ -3289,271 +3159,9 @@ BuildsOperationUpdateFolder::DownloadBuildBlob(uint32_t RemoteChunkInde
}
}
-BuildsOperationUpdateFolder::BlockRangeDescriptor
-BuildsOperationUpdateFolder::MergeBlockRanges(std::span<const BlockRangeDescriptor> Ranges)
-{
- ZEN_ASSERT(Ranges.size() > 1);
- const BlockRangeDescriptor& First = Ranges.front();
- const BlockRangeDescriptor& Last = Ranges.back();
-
- return BlockRangeDescriptor{.BlockIndex = First.BlockIndex,
- .RangeStart = First.RangeStart,
- .RangeLength = Last.RangeStart + Last.RangeLength - First.RangeStart,
- .ChunkBlockIndexStart = First.ChunkBlockIndexStart,
- .ChunkBlockIndexCount = Last.ChunkBlockIndexStart + Last.ChunkBlockIndexCount - First.ChunkBlockIndexStart};
-}
-
-std::optional<std::vector<BuildsOperationUpdateFolder::BlockRangeDescriptor>>
-BuildsOperationUpdateFolder::MakeOptionalBlockRangeVector(uint64_t TotalBlockSize, const BlockRangeDescriptor& Range)
-{
- if (Range.RangeLength == TotalBlockSize)
- {
- return {};
- }
- else
- {
- return std::vector<BlockRangeDescriptor>{Range};
- }
-};
-
-const BuildsOperationUpdateFolder::BlockRangeLimit*
-BuildsOperationUpdateFolder::GetBlockRangeLimitForRange(std::span<const BlockRangeLimit> Limits,
- uint64_t TotalBlockSize,
- std::span<const BlockRangeDescriptor> Ranges)
-{
- if (Ranges.size() > 1)
- {
- const std::uint64_t WantedSize =
- std::accumulate(Ranges.begin(), Ranges.end(), uint64_t(0), [](uint64_t Current, const BlockRangeDescriptor& Range) {
- return Current + Range.RangeLength;
- });
-
- const double RangeRequestedPercent = (WantedSize * 100.0) / TotalBlockSize;
-
- for (const BlockRangeLimit& Limit : Limits)
- {
- if (RangeRequestedPercent >= Limit.SizePercent && Ranges.size() > Limit.MaxRangeCount)
- {
- return &Limit;
- }
- }
- }
- return nullptr;
-};
-
-std::vector<BuildsOperationUpdateFolder::BlockRangeDescriptor>
-BuildsOperationUpdateFolder::CollapseBlockRanges(const uint64_t AlwaysAcceptableGap, std::span<const BlockRangeDescriptor> BlockRanges)
-{
- ZEN_ASSERT(BlockRanges.size() > 1);
- std::vector<BlockRangeDescriptor> CollapsedBlockRanges;
-
- auto BlockRangesIt = BlockRanges.begin();
- CollapsedBlockRanges.push_back(*BlockRangesIt++);
- for (; BlockRangesIt != BlockRanges.end(); BlockRangesIt++)
- {
- BlockRangeDescriptor& LastRange = CollapsedBlockRanges.back();
-
- const uint64_t BothRangeSize = BlockRangesIt->RangeLength + LastRange.RangeLength;
-
- const uint64_t Gap = BlockRangesIt->RangeStart - (LastRange.RangeStart + LastRange.RangeLength);
- if (Gap <= Max(BothRangeSize / 16, AlwaysAcceptableGap))
- {
- LastRange.ChunkBlockIndexCount =
- (BlockRangesIt->ChunkBlockIndexStart + BlockRangesIt->ChunkBlockIndexCount) - LastRange.ChunkBlockIndexStart;
- LastRange.RangeLength = (BlockRangesIt->RangeStart + BlockRangesIt->RangeLength) - LastRange.RangeStart;
- }
- else
- {
- CollapsedBlockRanges.push_back(*BlockRangesIt);
- }
- }
-
- return CollapsedBlockRanges;
-};
-
-uint64_t
-BuildsOperationUpdateFolder::CalculateNextGap(std::span<const BlockRangeDescriptor> BlockRanges)
-{
- ZEN_ASSERT(BlockRanges.size() > 1);
- uint64_t AcceptableGap = (uint64_t)-1;
- for (size_t RangeIndex = 0; RangeIndex < BlockRanges.size() - 1; RangeIndex++)
- {
- const BlockRangeDescriptor& Range = BlockRanges[RangeIndex];
- const BlockRangeDescriptor& NextRange = BlockRanges[RangeIndex + 1];
-
- const uint64_t Gap = NextRange.RangeStart - (Range.RangeStart + Range.RangeLength);
- AcceptableGap = Min(Gap, AcceptableGap);
- }
- AcceptableGap = RoundUp(AcceptableGap, 16u * 1024u);
- return AcceptableGap;
-};
-
-std::optional<std::vector<BuildsOperationUpdateFolder::BlockRangeDescriptor>>
-BuildsOperationUpdateFolder::CalculateBlockRanges(uint32_t BlockIndex,
- const ChunkBlockDescription& BlockDescription,
- std::span<const uint32_t> BlockChunkIndexNeeded,
- bool LimitToSingleRange,
- const uint64_t ChunkStartOffsetInBlock,
- const uint64_t TotalBlockSize,
- uint64_t& OutTotalWantedChunksSize)
-{
- ZEN_TRACE_CPU("CalculateBlockRanges");
-
- std::vector<BlockRangeDescriptor> BlockRanges;
- {
- uint64_t CurrentOffset = ChunkStartOffsetInBlock;
- uint32_t ChunkBlockIndex = 0;
- uint32_t NeedBlockChunkIndexOffset = 0;
- BlockRangeDescriptor NextRange{.BlockIndex = BlockIndex};
- while (NeedBlockChunkIndexOffset < BlockChunkIndexNeeded.size() && ChunkBlockIndex < BlockDescription.ChunkRawHashes.size())
- {
- const uint32_t ChunkCompressedLength = BlockDescription.ChunkCompressedLengths[ChunkBlockIndex];
- if (ChunkBlockIndex < BlockChunkIndexNeeded[NeedBlockChunkIndexOffset])
- {
- if (NextRange.RangeLength > 0)
- {
- BlockRanges.push_back(NextRange);
- NextRange = {.BlockIndex = BlockIndex};
- }
- ChunkBlockIndex++;
- CurrentOffset += ChunkCompressedLength;
- }
- else if (ChunkBlockIndex == BlockChunkIndexNeeded[NeedBlockChunkIndexOffset])
- {
- if (NextRange.RangeLength == 0)
- {
- NextRange.RangeStart = CurrentOffset;
- NextRange.ChunkBlockIndexStart = ChunkBlockIndex;
- }
- NextRange.RangeLength += ChunkCompressedLength;
- NextRange.ChunkBlockIndexCount++;
- ChunkBlockIndex++;
- CurrentOffset += ChunkCompressedLength;
- NeedBlockChunkIndexOffset++;
- }
- else
- {
- ZEN_ASSERT(false);
- }
- }
- if (NextRange.RangeLength > 0)
- {
- BlockRanges.push_back(NextRange);
- }
- }
- ZEN_ASSERT(!BlockRanges.empty());
-
- OutTotalWantedChunksSize =
- std::accumulate(BlockRanges.begin(), BlockRanges.end(), uint64_t(0), [](uint64_t Current, const BlockRangeDescriptor& Range) {
- return Current + Range.RangeLength;
- });
-
- double RangeWantedPercent = (OutTotalWantedChunksSize * 100.0) / TotalBlockSize;
-
- if (BlockRanges.size() == 1)
- {
- if (m_Options.IsVerbose)
- {
- ZEN_OPERATION_LOG_INFO(m_LogOutput,
- "Range request of {} ({:.2f}%) using single range from block {} ({}) as is",
- NiceBytes(OutTotalWantedChunksSize),
- RangeWantedPercent,
- BlockDescription.BlockHash,
- NiceBytes(TotalBlockSize));
- }
- return BlockRanges;
- }
-
- if (LimitToSingleRange)
- {
- const BlockRangeDescriptor MergedRange = MergeBlockRanges(BlockRanges);
- if (m_Options.IsVerbose)
- {
- const double RangeRequestedPercent = (MergedRange.RangeLength * 100.0) / TotalBlockSize;
- const double WastedPercent = ((MergedRange.RangeLength - OutTotalWantedChunksSize) * 100.0) / MergedRange.RangeLength;
-
- ZEN_OPERATION_LOG_INFO(
- m_LogOutput,
- "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) limited to single block range {} ({:.2f}%) wasting "
- "{:.2f}% ({})",
- NiceBytes(OutTotalWantedChunksSize),
- RangeWantedPercent,
- BlockRanges.size(),
- BlockDescription.BlockHash,
- NiceBytes(TotalBlockSize),
- NiceBytes(MergedRange.RangeLength),
- RangeRequestedPercent,
- WastedPercent,
- NiceBytes(MergedRange.RangeLength - OutTotalWantedChunksSize));
- }
- return MakeOptionalBlockRangeVector(TotalBlockSize, MergedRange);
- }
-
- if (RangeWantedPercent > FullBlockRangePercentLimit)
- {
- const BlockRangeDescriptor MergedRange = MergeBlockRanges(BlockRanges);
- if (m_Options.IsVerbose)
- {
- const double RangeRequestedPercent = (MergedRange.RangeLength * 100.0) / TotalBlockSize;
- const double WastedPercent = ((MergedRange.RangeLength - OutTotalWantedChunksSize) * 100.0) / MergedRange.RangeLength;
-
- ZEN_OPERATION_LOG_INFO(
- m_LogOutput,
- "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) exceeds {}%. Merged to single block range {} "
- "({:.2f}%) wasting {:.2f}% ({})",
- NiceBytes(OutTotalWantedChunksSize),
- RangeWantedPercent,
- BlockRanges.size(),
- BlockDescription.BlockHash,
- NiceBytes(TotalBlockSize),
- FullBlockRangePercentLimit,
- NiceBytes(MergedRange.RangeLength),
- RangeRequestedPercent,
- WastedPercent,
- NiceBytes(MergedRange.RangeLength - OutTotalWantedChunksSize));
- }
- return MakeOptionalBlockRangeVector(TotalBlockSize, MergedRange);
- }
-
- std::vector<BlockRangeDescriptor> CollapsedBlockRanges = CollapseBlockRanges(16u * 1024u, BlockRanges);
- while (GetBlockRangeLimitForRange(ForceMergeLimits, TotalBlockSize, CollapsedBlockRanges))
- {
- CollapsedBlockRanges = CollapseBlockRanges(CalculateNextGap(CollapsedBlockRanges), CollapsedBlockRanges);
- }
-
- const std::uint64_t WantedCollapsedSize =
- std::accumulate(CollapsedBlockRanges.begin(),
- CollapsedBlockRanges.end(),
- uint64_t(0),
- [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; });
-
- const double CollapsedRangeRequestedPercent = (WantedCollapsedSize * 100.0) / TotalBlockSize;
-
- if (m_Options.IsVerbose)
- {
- const double WastedPercent = ((WantedCollapsedSize - OutTotalWantedChunksSize) * 100.0) / WantedCollapsedSize;
-
- ZEN_OPERATION_LOG_INFO(
- m_LogOutput,
- "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) collapsed to {} {:.2f}% using {} ranges wasting {:.2f}% "
- "({})",
- NiceBytes(OutTotalWantedChunksSize),
- RangeWantedPercent,
- BlockRanges.size(),
- BlockDescription.BlockHash,
- NiceBytes(TotalBlockSize),
- NiceBytes(WantedCollapsedSize),
- CollapsedRangeRequestedPercent,
- CollapsedBlockRanges.size(),
- WastedPercent,
- NiceBytes(WantedCollapsedSize - OutTotalWantedChunksSize));
- }
- return CollapsedBlockRanges;
-}
-
void
BuildsOperationUpdateFolder::DownloadPartialBlock(
- const BlockRangeDescriptor BlockRange,
+ const ChunkBlockAnalyser::BlockRangeDescriptor BlockRange,
const BlobsExistsResult& ExistsResult,
std::function<void(IoBuffer&& InMemoryBuffer, const std::filesystem::path& OnDiskPath)>&& OnDownloaded)
{
diff --git a/src/zenremotestore/builds/buildstorageutil.cpp b/src/zenremotestore/builds/buildstorageutil.cpp
index 36b45e800..b249d7d52 100644
--- a/src/zenremotestore/builds/buildstorageutil.cpp
+++ b/src/zenremotestore/builds/buildstorageutil.cpp
@@ -63,11 +63,13 @@ ResolveBuildStorage(OperationLogOutput& Output,
std::string HostUrl;
std::string HostName;
+ double HostLatencySec = -1.0;
std::string CacheUrl;
std::string CacheName;
bool HostAssumeHttp2 = ClientSettings.AssumeHttp2;
bool CacheAssumeHttp2 = ClientSettings.AssumeHttp2;
+ double CacheLatencySec = -1.0;
JupiterServerDiscovery DiscoveryResponse;
const std::string_view DiscoveryHost = Host.empty() ? OverrideHost : Host;
@@ -98,8 +100,9 @@ ResolveBuildStorage(OperationLogOutput& Output,
{
ZEN_OPERATION_LOG_INFO(Output, "Server endpoint at '{}/api/v1/status/servers' succeeded", OverrideHost);
}
- HostUrl = OverrideHost;
- HostName = GetHostNameFromUrl(OverrideHost);
+ HostUrl = OverrideHost;
+ HostName = GetHostNameFromUrl(OverrideHost);
+ HostLatencySec = TestResult.LatencySeconds;
}
else
{
@@ -137,6 +140,7 @@ ResolveBuildStorage(OperationLogOutput& Output,
HostUrl = ServerEndpoint.BaseUrl;
HostAssumeHttp2 = ServerEndpoint.AssumeHttp2;
HostName = ServerEndpoint.Name;
+ HostLatencySec = TestResult.LatencySeconds;
break;
}
else
@@ -183,6 +187,7 @@ ResolveBuildStorage(OperationLogOutput& Output,
CacheUrl = CacheEndpoint.BaseUrl;
CacheAssumeHttp2 = CacheEndpoint.AssumeHttp2;
CacheName = CacheEndpoint.Name;
+ CacheLatencySec = TestResult.LatencySeconds;
break;
}
}
@@ -204,6 +209,7 @@ ResolveBuildStorage(OperationLogOutput& Output,
CacheUrl = ZenServerLocalHostUrl;
CacheAssumeHttp2 = false;
CacheName = "localhost";
+ CacheLatencySec = TestResult.LatencySeconds;
}
}
});
@@ -219,8 +225,9 @@ ResolveBuildStorage(OperationLogOutput& Output,
if (ZenCacheEndpointTestResult TestResult = TestZenCacheEndpoint(ZenCacheHost, /*AssumeHttp2*/ false, ClientSettings.Verbose);
TestResult.Success)
{
- CacheUrl = ZenCacheHost;
- CacheName = GetHostNameFromUrl(ZenCacheHost);
+ CacheUrl = ZenCacheHost;
+ CacheName = GetHostNameFromUrl(ZenCacheHost);
+ CacheLatencySec = TestResult.LatencySeconds;
}
else
{
@@ -231,10 +238,12 @@ ResolveBuildStorage(OperationLogOutput& Output,
return BuildStorageResolveResult{.HostUrl = HostUrl,
.HostName = HostName,
.HostAssumeHttp2 = HostAssumeHttp2,
+ .HostLatencySec = HostLatencySec,
.CacheUrl = CacheUrl,
.CacheName = CacheName,
- .CacheAssumeHttp2 = CacheAssumeHttp2};
+ .CacheAssumeHttp2 = CacheAssumeHttp2,
+ .CacheLatencySec = CacheLatencySec};
}
std::vector<ChunkBlockDescription>
diff --git a/src/zenremotestore/chunking/chunkblock.cpp b/src/zenremotestore/chunking/chunkblock.cpp
index c4d8653f4..d203e0292 100644
--- a/src/zenremotestore/chunking/chunkblock.cpp
+++ b/src/zenremotestore/chunking/chunkblock.cpp
@@ -10,18 +10,17 @@
#include <zenremotestore/operationlogoutput.h>
+#include <numeric>
#include <vector>
ZEN_THIRD_PARTY_INCLUDES_START
#include <tsl/robin_map.h>
+#include <tsl/robin_set.h>
ZEN_THIRD_PARTY_INCLUDES_END
#if ZEN_WITH_TESTS
# include <zencore/testing.h>
# include <zencore/testutils.h>
-
-# include <unordered_map>
-# include <numeric>
#endif // ZEN_WITH_TESTS
namespace zen {
@@ -455,6 +454,578 @@ FindReuseBlocks(OperationLogOutput& Output,
return FilteredReuseBlockIndexes;
}
+ChunkBlockAnalyser::ChunkBlockAnalyser(OperationLogOutput& LogOutput,
+ std::span<const ChunkBlockDescription> BlockDescriptions,
+ const Options& Options)
+: m_LogOutput(LogOutput)
+, m_BlockDescriptions(BlockDescriptions)
+, m_Options(Options)
+{
+}
+
+std::vector<ChunkBlockAnalyser::NeededBlock>
+ChunkBlockAnalyser::GetNeeded(const tsl::robin_map<IoHash, uint32_t, IoHash::Hasher>& ChunkHashToChunkIndex,
+ std::function<bool(uint32_t ChunkIndex)>&& NeedsBlockChunk)
+{
+ ZEN_TRACE_CPU("ChunkBlockAnalyser::GetNeeded");
+
+ std::vector<NeededBlock> Result;
+
+ std::vector<bool> ChunkIsNeeded(ChunkHashToChunkIndex.size());
+ for (uint32_t ChunkIndex = 0; ChunkIndex < ChunkHashToChunkIndex.size(); ChunkIndex++)
+ {
+ ChunkIsNeeded[ChunkIndex] = NeedsBlockChunk(ChunkIndex);
+ }
+
+ std::vector<uint64_t> BlockSlack(m_BlockDescriptions.size(), 0u);
+ for (uint32_t BlockIndex = 0; BlockIndex < m_BlockDescriptions.size(); BlockIndex++)
+ {
+ const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex];
+
+ uint64_t BlockUsedSize = 0;
+ uint64_t BlockSize = 0;
+
+ for (uint32_t ChunkBlockIndex = 0; ChunkBlockIndex < BlockDescription.ChunkRawHashes.size(); ChunkBlockIndex++)
+ {
+ const IoHash& ChunkHash = BlockDescription.ChunkRawHashes[ChunkBlockIndex];
+ if (auto It = ChunkHashToChunkIndex.find(ChunkHash); It != ChunkHashToChunkIndex.end())
+ {
+ const uint32_t RemoteChunkIndex = It->second;
+ if (ChunkIsNeeded[RemoteChunkIndex])
+ {
+ BlockUsedSize += BlockDescription.ChunkCompressedLengths[ChunkBlockIndex];
+ }
+ }
+ BlockSize += BlockDescription.ChunkCompressedLengths[ChunkBlockIndex];
+ }
+ BlockSlack[BlockIndex] = BlockSize - BlockUsedSize;
+ }
+
+ std::vector<uint32_t> BlockOrder(m_BlockDescriptions.size());
+ std::iota(BlockOrder.begin(), BlockOrder.end(), 0);
+
+ std::sort(BlockOrder.begin(), BlockOrder.end(), [&BlockSlack](uint32_t Lhs, uint32_t Rhs) {
+ return BlockSlack[Lhs] < BlockSlack[Rhs];
+ });
+
+ std::vector<bool> ChunkIsPickedUp(ChunkHashToChunkIndex.size(), false);
+
+ for (uint32_t BlockIndex : BlockOrder)
+ {
+ const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex];
+
+ std::vector<uint32_t> BlockChunkIndexNeeded;
+
+ for (uint32_t ChunkBlockIndex = 0; ChunkBlockIndex < BlockDescription.ChunkRawHashes.size(); ChunkBlockIndex++)
+ {
+ const IoHash& ChunkHash = BlockDescription.ChunkRawHashes[ChunkBlockIndex];
+ if (auto It = ChunkHashToChunkIndex.find(ChunkHash); It != ChunkHashToChunkIndex.end())
+ {
+ const uint32_t RemoteChunkIndex = It->second;
+ if (ChunkIsNeeded[RemoteChunkIndex])
+ {
+ if (!ChunkIsPickedUp[RemoteChunkIndex])
+ {
+ ChunkIsPickedUp[RemoteChunkIndex] = true;
+ BlockChunkIndexNeeded.push_back(ChunkBlockIndex);
+ }
+ }
+ }
+ else
+ {
+ ZEN_DEBUG("Chunk {} not found in block {}", ChunkHash, BlockDescription.BlockHash);
+ }
+ }
+
+ if (!BlockChunkIndexNeeded.empty())
+ {
+ Result.push_back(NeededBlock{.BlockIndex = BlockIndex, .ChunkIndexes = std::move(BlockChunkIndexNeeded)});
+ }
+ }
+ return Result;
+}
+
+ChunkBlockAnalyser::BlockResult
+ChunkBlockAnalyser::CalculatePartialBlockDownloads(std::span<const NeededBlock> NeededBlocks,
+ std::span<const EPartialBlockDownloadMode> BlockPartialDownloadModes)
+{
+ ZEN_TRACE_CPU("ChunkBlockAnalyser::CalculatePartialBlockDownloads");
+
+ Stopwatch PartialAnalisysTimer;
+
+ ChunkBlockAnalyser::BlockResult Result;
+
+ uint64_t IdealDownloadTotalSize = 0;
+ uint64_t AllBlocksTotalBlocksSize = 0;
+
+ for (const NeededBlock& NeededBlock : NeededBlocks)
+ {
+ const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[NeededBlock.BlockIndex];
+
+ std::span<const uint32_t> BlockChunkIndexNeeded(NeededBlock.ChunkIndexes);
+ if (!NeededBlock.ChunkIndexes.empty())
+ {
+ bool WantsToDoPartialBlockDownload = NeededBlock.ChunkIndexes.size() < BlockDescription.ChunkRawHashes.size();
+ bool CanDoPartialBlockDownload = (BlockDescription.HeaderSize > 0) &&
+ (BlockDescription.ChunkCompressedLengths.size() == BlockDescription.ChunkRawHashes.size());
+
+ EPartialBlockDownloadMode PartialBlockDownloadMode = BlockPartialDownloadModes[NeededBlock.BlockIndex];
+
+ const uint32_t ChunkStartOffsetInBlock =
+ gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize);
+
+ const uint64_t TotalBlockSize = std::accumulate(BlockDescription.ChunkCompressedLengths.begin(),
+ BlockDescription.ChunkCompressedLengths.end(),
+ std::uint64_t(ChunkStartOffsetInBlock));
+
+ AllBlocksTotalBlocksSize += TotalBlockSize;
+
+ if ((PartialBlockDownloadMode != EPartialBlockDownloadMode::Off) && WantsToDoPartialBlockDownload && CanDoPartialBlockDownload)
+ {
+ ZEN_TRACE_CPU("PartialBlockAnalysis");
+
+ uint64_t TotalWantedChunksSize = 0;
+ std::optional<std::vector<BlockRangeDescriptor>> MaybeBlockRanges = CalculateBlockRanges(NeededBlock.BlockIndex,
+ BlockDescription,
+ NeededBlock.ChunkIndexes,
+ PartialBlockDownloadMode,
+ ChunkStartOffsetInBlock,
+ TotalBlockSize,
+ TotalWantedChunksSize);
+ ZEN_ASSERT(TotalWantedChunksSize <= TotalBlockSize);
+ IdealDownloadTotalSize += TotalWantedChunksSize;
+
+ if (MaybeBlockRanges.has_value())
+ {
+ std::vector<BlockRangeDescriptor> BlockRanges = MaybeBlockRanges.value();
+ ZEN_ASSERT(!BlockRanges.empty());
+
+ uint64_t RequestedSize =
+ std::accumulate(BlockRanges.begin(),
+ BlockRanges.end(),
+ uint64_t(0),
+ [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; });
+
+ if (PartialBlockDownloadMode != EPartialBlockDownloadMode::Exact && BlockRanges.size() > 1)
+ {
+ // TODO: Once we have support in our http client to request multiple ranges in one request this
+ // logic would need to change as the per-request overhead would go away
+
+ const double LatencySec = PartialBlockDownloadMode == EPartialBlockDownloadMode::MultiRangeHighSpeed
+ ? m_Options.HostHighSpeedLatencySec
+ : m_Options.HostLatencySec;
+ if (LatencySec > 0)
+ {
+ const uint64_t BytesPerSec = PartialBlockDownloadMode == EPartialBlockDownloadMode::MultiRangeHighSpeed
+ ? m_Options.HostHighSpeedBytesPerSec
+ : m_Options.HostSpeedBytesPerSec;
+
+ const double ExtraRequestTimeSec = (BlockRanges.size() - 1) * LatencySec;
+ const uint64_t ExtraRequestTimeBytes = uint64_t(ExtraRequestTimeSec * BytesPerSec);
+
+ const uint64_t FullRangeSize =
+ BlockRanges.back().RangeStart + BlockRanges.back().RangeLength - BlockRanges.front().RangeStart;
+
+ if (ExtraRequestTimeBytes + RequestedSize >= FullRangeSize)
+ {
+ BlockRanges = std::vector<BlockRangeDescriptor>{MergeBlockRanges(BlockRanges)};
+
+ if (m_Options.IsVerbose)
+ {
+ ZEN_OPERATION_LOG_INFO(m_LogOutput,
+ "Merging {} chunks ({}) from block {} ({}) to single request (extra bytes {})",
+ NeededBlock.ChunkIndexes.size(),
+ NiceBytes(RequestedSize),
+ BlockDescription.BlockHash,
+ NiceBytes(TotalBlockSize),
+ NiceBytes(BlockRanges.front().RangeLength - RequestedSize));
+ }
+
+ RequestedSize = BlockRanges.front().RangeLength;
+ }
+ }
+ }
+
+ if ((PartialBlockDownloadMode != EPartialBlockDownloadMode::Exact) &&
+ ((TotalBlockSize - RequestedSize) < (512u * 1024u)))
+ {
+ if (m_Options.IsVerbose)
+ {
+ ZEN_OPERATION_LOG_INFO(m_LogOutput,
+ "Requesting {} chunks ({}) from block {} ({}) using full block request due to small "
+ "total slack (extra bytes {})",
+ NeededBlock.ChunkIndexes.size(),
+ NiceBytes(RequestedSize),
+ BlockDescription.BlockHash,
+ NiceBytes(TotalBlockSize),
+ NiceBytes(TotalBlockSize - TotalWantedChunksSize));
+ }
+ Result.FullBlockIndexes.push_back(NeededBlock.BlockIndex);
+ }
+ else
+ {
+ Result.BlockRanges.insert(Result.BlockRanges.end(), BlockRanges.begin(), BlockRanges.end());
+
+ if (m_Options.IsVerbose)
+ {
+ ZEN_OPERATION_LOG_INFO(m_LogOutput,
+ "Requesting {} chunks ({}) from block {} ({}) using {} requests (extra bytes {})",
+ NeededBlock.ChunkIndexes.size(),
+ NiceBytes(RequestedSize),
+ BlockDescription.BlockHash,
+ NiceBytes(TotalBlockSize),
+ BlockRanges.size(),
+ NiceBytes(RequestedSize - TotalWantedChunksSize));
+ }
+ }
+ }
+ else
+ {
+ Result.FullBlockIndexes.push_back(NeededBlock.BlockIndex);
+ }
+ }
+ else
+ {
+ Result.FullBlockIndexes.push_back(NeededBlock.BlockIndex);
+ IdealDownloadTotalSize += TotalBlockSize;
+ }
+ }
+ }
+
+ if (!Result.BlockRanges.empty() && !m_Options.IsQuiet)
+ {
+ tsl::robin_set<uint32_t> PartialBlockIndexes;
+ uint64_t PartialBlocksTotalSize = std::accumulate(Result.BlockRanges.begin(),
+ Result.BlockRanges.end(),
+ uint64_t(0u),
+ [&](uint64_t Current, const BlockRangeDescriptor& Range) {
+ PartialBlockIndexes.insert(Range.BlockIndex);
+ return Current + Range.RangeLength;
+ });
+
+ uint64_t FullBlocksTotalSize =
+ std::accumulate(Result.FullBlockIndexes.begin(),
+ Result.FullBlockIndexes.end(),
+ uint64_t(0u),
+ [&](uint64_t Current, uint32_t BlockIndex) {
+ const ChunkBlockDescription& BlockDescription = m_BlockDescriptions[BlockIndex];
+ uint32_t CurrentOffset =
+ gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize);
+
+ return Current + std::accumulate(BlockDescription.ChunkCompressedLengths.begin(),
+ BlockDescription.ChunkCompressedLengths.end(),
+ std::uint64_t(CurrentOffset));
+ });
+
+ uint64_t PartialBlockRequestCount = Result.BlockRanges.size();
+ uint64_t PartialBlockCount = PartialBlockIndexes.size();
+
+ uint64_t TotalExtraPartialBlocksRequestCount = PartialBlockRequestCount - PartialBlockCount;
+ uint64_t ActualPartialDownloadTotalSize = FullBlocksTotalSize + PartialBlocksTotalSize;
+
+ uint64_t IdealSkippedSize = AllBlocksTotalBlocksSize - IdealDownloadTotalSize;
+ uint64_t ActualSkippedSize = AllBlocksTotalBlocksSize - ActualPartialDownloadTotalSize;
+
+ double PercentOfIdealPartialSkippedSize = (ActualSkippedSize * 100.0) / IdealSkippedSize;
+
+ ZEN_OPERATION_LOG_INFO(m_LogOutput,
+ "Analysis of partial block requests saves download of {} out of {}, {:.1f}% of possible {} using {} extra "
+ "requests. Completed in {}",
+ NiceBytes(ActualSkippedSize),
+ NiceBytes(AllBlocksTotalBlocksSize),
+ PercentOfIdealPartialSkippedSize,
+ NiceBytes(IdealSkippedSize),
+ TotalExtraPartialBlocksRequestCount,
+ NiceTimeSpanMs(PartialAnalisysTimer.GetElapsedTimeMs()));
+ }
+
+ return Result;
+}
+
+ChunkBlockAnalyser::BlockRangeDescriptor
+ChunkBlockAnalyser::MergeBlockRanges(std::span<const BlockRangeDescriptor> Ranges)
+{
+ ZEN_ASSERT(Ranges.size() > 1);
+ const BlockRangeDescriptor& First = Ranges.front();
+ const BlockRangeDescriptor& Last = Ranges.back();
+
+ return BlockRangeDescriptor{.BlockIndex = First.BlockIndex,
+ .RangeStart = First.RangeStart,
+ .RangeLength = Last.RangeStart + Last.RangeLength - First.RangeStart,
+ .ChunkBlockIndexStart = First.ChunkBlockIndexStart,
+ .ChunkBlockIndexCount = Last.ChunkBlockIndexStart + Last.ChunkBlockIndexCount - First.ChunkBlockIndexStart};
+}
+
+std::optional<std::vector<ChunkBlockAnalyser::BlockRangeDescriptor>>
+ChunkBlockAnalyser::MakeOptionalBlockRangeVector(uint64_t TotalBlockSize, const BlockRangeDescriptor& Range)
+{
+ if (Range.RangeLength == TotalBlockSize)
+ {
+ return {};
+ }
+ else
+ {
+ return std::vector<BlockRangeDescriptor>{Range};
+ }
+};
+
+const ChunkBlockAnalyser::BlockRangeLimit*
+ChunkBlockAnalyser::GetBlockRangeLimitForRange(std::span<const BlockRangeLimit> Limits,
+ uint64_t TotalBlockSize,
+ std::span<const BlockRangeDescriptor> Ranges)
+{
+ if (Ranges.size() > 1)
+ {
+ const std::uint64_t WantedSize =
+ std::accumulate(Ranges.begin(), Ranges.end(), uint64_t(0), [](uint64_t Current, const BlockRangeDescriptor& Range) {
+ return Current + Range.RangeLength;
+ });
+
+ const double RangeRequestedPercent = (WantedSize * 100.0) / TotalBlockSize;
+
+ for (const BlockRangeLimit& Limit : Limits)
+ {
+ if (RangeRequestedPercent >= Limit.SizePercent && Ranges.size() > Limit.MaxRangeCount)
+ {
+ return &Limit;
+ }
+ }
+ }
+ return nullptr;
+};
+
+std::vector<ChunkBlockAnalyser::BlockRangeDescriptor>
+ChunkBlockAnalyser::CollapseBlockRanges(const uint64_t AlwaysAcceptableGap, std::span<const BlockRangeDescriptor> BlockRanges)
+{
+ ZEN_ASSERT(BlockRanges.size() > 1);
+ std::vector<BlockRangeDescriptor> CollapsedBlockRanges;
+
+ auto BlockRangesIt = BlockRanges.begin();
+ CollapsedBlockRanges.push_back(*BlockRangesIt++);
+ for (; BlockRangesIt != BlockRanges.end(); BlockRangesIt++)
+ {
+ BlockRangeDescriptor& LastRange = CollapsedBlockRanges.back();
+
+ const uint64_t BothRangeSize = BlockRangesIt->RangeLength + LastRange.RangeLength;
+
+ const uint64_t Gap = BlockRangesIt->RangeStart - (LastRange.RangeStart + LastRange.RangeLength);
+ if (Gap <= Max(BothRangeSize / 16, AlwaysAcceptableGap))
+ {
+ LastRange.ChunkBlockIndexCount =
+ (BlockRangesIt->ChunkBlockIndexStart + BlockRangesIt->ChunkBlockIndexCount) - LastRange.ChunkBlockIndexStart;
+ LastRange.RangeLength = (BlockRangesIt->RangeStart + BlockRangesIt->RangeLength) - LastRange.RangeStart;
+ }
+ else
+ {
+ CollapsedBlockRanges.push_back(*BlockRangesIt);
+ }
+ }
+
+ return CollapsedBlockRanges;
+};
+
+uint64_t
+ChunkBlockAnalyser::CalculateNextGap(const uint64_t AlwaysAcceptableGap, std::span<const BlockRangeDescriptor> BlockRanges)
+{
+ ZEN_ASSERT(BlockRanges.size() > 1);
+ uint64_t AcceptableGap = (uint64_t)-1;
+ for (size_t RangeIndex = 0; RangeIndex < BlockRanges.size() - 1; RangeIndex++)
+ {
+ const BlockRangeDescriptor& Range = BlockRanges[RangeIndex];
+ const BlockRangeDescriptor& NextRange = BlockRanges[RangeIndex + 1];
+
+ const uint64_t Gap = NextRange.RangeStart - (Range.RangeStart + Range.RangeLength);
+ AcceptableGap = Min(Gap, AcceptableGap);
+ }
+ AcceptableGap = RoundUp(AcceptableGap, AlwaysAcceptableGap);
+ return AcceptableGap;
+};
+
+std::optional<std::vector<ChunkBlockAnalyser::BlockRangeDescriptor>>
+ChunkBlockAnalyser::CalculateBlockRanges(uint32_t BlockIndex,
+ const ChunkBlockDescription& BlockDescription,
+ std::span<const uint32_t> BlockChunkIndexNeeded,
+ EPartialBlockDownloadMode PartialBlockDownloadMode,
+ const uint64_t ChunkStartOffsetInBlock,
+ const uint64_t TotalBlockSize,
+ uint64_t& OutTotalWantedChunksSize)
+{
+ ZEN_TRACE_CPU("CalculateBlockRanges");
+
+ if (PartialBlockDownloadMode == EPartialBlockDownloadMode::Off)
+ {
+ return {};
+ }
+
+ std::vector<BlockRangeDescriptor> BlockRanges;
+ {
+ uint64_t CurrentOffset = ChunkStartOffsetInBlock;
+ uint32_t ChunkBlockIndex = 0;
+ uint32_t NeedBlockChunkIndexOffset = 0;
+ BlockRangeDescriptor NextRange{.BlockIndex = BlockIndex};
+ while (NeedBlockChunkIndexOffset < BlockChunkIndexNeeded.size() && ChunkBlockIndex < BlockDescription.ChunkRawHashes.size())
+ {
+ const uint32_t ChunkCompressedLength = BlockDescription.ChunkCompressedLengths[ChunkBlockIndex];
+ if (ChunkBlockIndex < BlockChunkIndexNeeded[NeedBlockChunkIndexOffset])
+ {
+ if (NextRange.RangeLength > 0)
+ {
+ BlockRanges.push_back(NextRange);
+ NextRange = {.BlockIndex = BlockIndex};
+ }
+ ChunkBlockIndex++;
+ CurrentOffset += ChunkCompressedLength;
+ }
+ else if (ChunkBlockIndex == BlockChunkIndexNeeded[NeedBlockChunkIndexOffset])
+ {
+ if (NextRange.RangeLength == 0)
+ {
+ NextRange.RangeStart = CurrentOffset;
+ NextRange.ChunkBlockIndexStart = ChunkBlockIndex;
+ }
+ NextRange.RangeLength += ChunkCompressedLength;
+ NextRange.ChunkBlockIndexCount++;
+ ChunkBlockIndex++;
+ CurrentOffset += ChunkCompressedLength;
+ NeedBlockChunkIndexOffset++;
+ }
+ else
+ {
+ ZEN_ASSERT(false);
+ }
+ }
+ if (NextRange.RangeLength > 0)
+ {
+ BlockRanges.push_back(NextRange);
+ }
+ }
+ ZEN_ASSERT(!BlockRanges.empty());
+
+ OutTotalWantedChunksSize =
+ std::accumulate(BlockRanges.begin(), BlockRanges.end(), uint64_t(0), [](uint64_t Current, const BlockRangeDescriptor& Range) {
+ return Current + Range.RangeLength;
+ });
+
+ double RangeWantedPercent = (OutTotalWantedChunksSize * 100.0) / TotalBlockSize;
+
+ if (BlockRanges.size() == 1)
+ {
+ if (m_Options.IsVerbose)
+ {
+ ZEN_OPERATION_LOG_INFO(m_LogOutput,
+ "Range request of {} ({:.2f}%) using single range from block {} ({}) as is",
+ NiceBytes(OutTotalWantedChunksSize),
+ RangeWantedPercent,
+ BlockDescription.BlockHash,
+ NiceBytes(TotalBlockSize));
+ }
+ return BlockRanges;
+ }
+
+ if (PartialBlockDownloadMode == EPartialBlockDownloadMode::Exact)
+ {
+ if (m_Options.IsVerbose)
+ {
+ ZEN_OPERATION_LOG_INFO(m_LogOutput,
+ "Range request of {} ({:.2f}%) using {} ranges from block {} ({})",
+ NiceBytes(OutTotalWantedChunksSize),
+ RangeWantedPercent,
+ BlockRanges.size(),
+ BlockDescription.BlockHash,
+ NiceBytes(TotalBlockSize));
+ }
+ return BlockRanges;
+ }
+
+ if (PartialBlockDownloadMode == EPartialBlockDownloadMode::SingleRange)
+ {
+ const BlockRangeDescriptor MergedRange = MergeBlockRanges(BlockRanges);
+ if (m_Options.IsVerbose)
+ {
+ const double RangeRequestedPercent = (MergedRange.RangeLength * 100.0) / TotalBlockSize;
+ const double WastedPercent = ((MergedRange.RangeLength - OutTotalWantedChunksSize) * 100.0) / MergedRange.RangeLength;
+
+ ZEN_OPERATION_LOG_INFO(
+ m_LogOutput,
+ "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) limited to single block range {} ({:.2f}%) wasting "
+ "{:.2f}% ({})",
+ NiceBytes(OutTotalWantedChunksSize),
+ RangeWantedPercent,
+ BlockRanges.size(),
+ BlockDescription.BlockHash,
+ NiceBytes(TotalBlockSize),
+ NiceBytes(MergedRange.RangeLength),
+ RangeRequestedPercent,
+ WastedPercent,
+ NiceBytes(MergedRange.RangeLength - OutTotalWantedChunksSize));
+ }
+ return MakeOptionalBlockRangeVector(TotalBlockSize, MergedRange);
+ }
+
+ if (RangeWantedPercent > FullBlockRangePercentLimit)
+ {
+ const BlockRangeDescriptor MergedRange = MergeBlockRanges(BlockRanges);
+ if (m_Options.IsVerbose)
+ {
+ const double RangeRequestedPercent = (MergedRange.RangeLength * 100.0) / TotalBlockSize;
+ const double WastedPercent = ((MergedRange.RangeLength - OutTotalWantedChunksSize) * 100.0) / MergedRange.RangeLength;
+
+ ZEN_OPERATION_LOG_INFO(
+ m_LogOutput,
+ "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) exceeds {}%. Merged to single block range {} "
+ "({:.2f}%) wasting {:.2f}% ({})",
+ NiceBytes(OutTotalWantedChunksSize),
+ RangeWantedPercent,
+ BlockRanges.size(),
+ BlockDescription.BlockHash,
+ NiceBytes(TotalBlockSize),
+ FullBlockRangePercentLimit,
+ NiceBytes(MergedRange.RangeLength),
+ RangeRequestedPercent,
+ WastedPercent,
+ NiceBytes(MergedRange.RangeLength - OutTotalWantedChunksSize));
+ }
+ return MakeOptionalBlockRangeVector(TotalBlockSize, MergedRange);
+ }
+
+ const uint64_t AlwaysAcceptableGap = 4u * 1024u;
+
+ std::vector<BlockRangeDescriptor> CollapsedBlockRanges = CollapseBlockRanges(AlwaysAcceptableGap, BlockRanges);
+ while (GetBlockRangeLimitForRange(ForceMergeLimits, TotalBlockSize, CollapsedBlockRanges))
+ {
+ CollapsedBlockRanges = CollapseBlockRanges(CalculateNextGap(AlwaysAcceptableGap, CollapsedBlockRanges), CollapsedBlockRanges);
+ }
+
+ const std::uint64_t WantedCollapsedSize =
+ std::accumulate(CollapsedBlockRanges.begin(),
+ CollapsedBlockRanges.end(),
+ uint64_t(0),
+ [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; });
+
+ const double CollapsedRangeRequestedPercent = (WantedCollapsedSize * 100.0) / TotalBlockSize;
+
+ if (m_Options.IsVerbose)
+ {
+ const double WastedPercent = ((WantedCollapsedSize - OutTotalWantedChunksSize) * 100.0) / WantedCollapsedSize;
+
+ ZEN_OPERATION_LOG_INFO(
+ m_LogOutput,
+ "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) collapsed to {} {:.2f}% using {} ranges wasting {:.2f}% "
+ "({})",
+ NiceBytes(OutTotalWantedChunksSize),
+ RangeWantedPercent,
+ BlockRanges.size(),
+ BlockDescription.BlockHash,
+ NiceBytes(TotalBlockSize),
+ NiceBytes(WantedCollapsedSize),
+ CollapsedRangeRequestedPercent,
+ CollapsedBlockRanges.size(),
+ WastedPercent,
+ NiceBytes(WantedCollapsedSize - OutTotalWantedChunksSize));
+ }
+ return CollapsedBlockRanges;
+}
+
#if ZEN_WITH_TESTS
namespace testutils {
@@ -476,7 +1047,7 @@ namespace testutils {
} // namespace testutils
-TEST_CASE("project.store.block")
+TEST_CASE("chunkblock.block")
{
using namespace std::literals;
using namespace testutils;
@@ -504,7 +1075,7 @@ TEST_CASE("project.store.block")
HeaderSize));
}
-TEST_CASE("project.store.reuseblocks")
+TEST_CASE("chunkblock.reuseblocks")
{
using namespace std::literals;
using namespace testutils;
diff --git a/src/zenremotestore/include/zenremotestore/builds/buildstoragecache.h b/src/zenremotestore/include/zenremotestore/builds/buildstoragecache.h
index bb5b1c5f4..f25ce5b5e 100644
--- a/src/zenremotestore/include/zenremotestore/builds/buildstoragecache.h
+++ b/src/zenremotestore/include/zenremotestore/builds/buildstoragecache.h
@@ -65,6 +65,7 @@ struct ZenCacheEndpointTestResult
{
bool Success = false;
std::string FailureReason;
+ double LatencySeconds = -1.0;
};
ZenCacheEndpointTestResult TestZenCacheEndpoint(std::string_view BaseUrl, const bool AssumeHttp2, const bool HttpVerbose);
diff --git a/src/zenremotestore/include/zenremotestore/builds/buildstorageoperations.h b/src/zenremotestore/include/zenremotestore/builds/buildstorageoperations.h
index 9e5bf8d91..31733569e 100644
--- a/src/zenremotestore/include/zenremotestore/builds/buildstorageoperations.h
+++ b/src/zenremotestore/include/zenremotestore/builds/buildstorageoperations.h
@@ -7,7 +7,9 @@
#include <zencore/uid.h>
#include <zencore/zencore.h>
#include <zenremotestore/builds/buildstoragecache.h>
+#include <zenremotestore/chunking/chunkblock.h>
#include <zenremotestore/chunking/chunkedcontent.h>
+#include <zenremotestore/partialblockrequestmode.h>
#include <zenutil/bufferedwritefilecache.h>
#include <atomic>
@@ -108,17 +110,6 @@ struct RebuildFolderStateStatistics
uint64_t FinalizeTreeElapsedWallTimeUs = 0;
};
-enum EPartialBlockRequestMode
-{
- Off,
- ZenCacheOnly,
- Mixed,
- All,
- Invalid
-};
-
-EPartialBlockRequestMode PartialBlockRequestModeFromString(const std::string_view ModeString);
-
std::filesystem::path ZenStateFilePath(const std::filesystem::path& ZenFolderPath);
std::filesystem::path ZenTempFolderPath(const std::filesystem::path& ZenFolderPath);
@@ -218,33 +209,6 @@ private:
uint64_t ElapsedTimeMs = 0;
};
- struct BlockRangeDescriptor
- {
- uint32_t BlockIndex = (uint32_t)-1;
- uint64_t RangeStart = 0;
- uint64_t RangeLength = 0;
- uint32_t ChunkBlockIndexStart = 0;
- uint32_t ChunkBlockIndexCount = 0;
- };
-
- struct BlockRangeLimit
- {
- uint16_t SizePercent;
- uint16_t MaxRangeCount;
- };
-
- static constexpr uint16_t FullBlockRangePercentLimit = 95;
-
- static constexpr BuildsOperationUpdateFolder::BlockRangeLimit ForceMergeLimits[] = {
- {.SizePercent = FullBlockRangePercentLimit, .MaxRangeCount = 1},
- {.SizePercent = 90, .MaxRangeCount = 2},
- {.SizePercent = 85, .MaxRangeCount = 8},
- {.SizePercent = 80, .MaxRangeCount = 16},
- {.SizePercent = 70, .MaxRangeCount = 32},
- {.SizePercent = 60, .MaxRangeCount = 48},
- {.SizePercent = 2, .MaxRangeCount = 56},
- {.SizePercent = 0, .MaxRangeCount = 64}};
-
void ScanCacheFolder(tsl::robin_map<IoHash, uint32_t, IoHash::Hasher>& OutCachedChunkHashesFound,
tsl::robin_map<IoHash, uint32_t, IoHash::Hasher>& OutCachedSequenceHashesFound);
void ScanTempBlocksFolder(tsl::robin_map<IoHash, uint32_t, IoHash::Hasher>& OutCachedBlocksFound);
@@ -299,25 +263,9 @@ private:
ParallelWork& Work,
std::function<void(IoBuffer&& Payload)>&& OnDownloaded);
- BlockRangeDescriptor MergeBlockRanges(std::span<const BlockRangeDescriptor> Ranges);
- std::optional<std::vector<BlockRangeDescriptor>> MakeOptionalBlockRangeVector(uint64_t TotalBlockSize,
- const BlockRangeDescriptor& Range);
- const BlockRangeLimit* GetBlockRangeLimitForRange(std::span<const BlockRangeLimit> Limits,
- uint64_t TotalBlockSize,
- std::span<const BlockRangeDescriptor> Ranges);
- std::vector<BlockRangeDescriptor> CollapseBlockRanges(const uint64_t AlwaysAcceptableGap,
- std::span<const BlockRangeDescriptor> BlockRanges);
- uint64_t CalculateNextGap(std::span<const BlockRangeDescriptor> BlockRanges);
- std::optional<std::vector<BlockRangeDescriptor>> CalculateBlockRanges(uint32_t BlockIndex,
- const ChunkBlockDescription& BlockDescription,
- std::span<const uint32_t> BlockChunkIndexNeeded,
- bool LimitToSingleRange,
- const uint64_t ChunkStartOffsetInBlock,
- const uint64_t TotalBlockSize,
- uint64_t& OutTotalWantedChunksSize);
- void DownloadPartialBlock(const BlockRangeDescriptor BlockRange,
- const BlobsExistsResult& ExistsResult,
- std::function<void(IoBuffer&& InMemoryBuffer, const std::filesystem::path& OnDiskPath)>&& OnDownloaded);
+ void DownloadPartialBlock(const ChunkBlockAnalyser::BlockRangeDescriptor BlockRange,
+ const BlobsExistsResult& ExistsResult,
+ std::function<void(IoBuffer&& InMemoryBuffer, const std::filesystem::path& OnDiskPath)>&& OnDownloaded);
std::vector<uint32_t> WriteLocalChunkToCache(CloneQueryInterface* CloneQuery,
const CopyChunkData& CopyData,
diff --git a/src/zenremotestore/include/zenremotestore/builds/buildstorageutil.h b/src/zenremotestore/include/zenremotestore/builds/buildstorageutil.h
index ab3037c89..4b85d8f1e 100644
--- a/src/zenremotestore/include/zenremotestore/builds/buildstorageutil.h
+++ b/src/zenremotestore/include/zenremotestore/builds/buildstorageutil.h
@@ -17,10 +17,12 @@ struct BuildStorageResolveResult
std::string HostUrl;
std::string HostName;
bool HostAssumeHttp2 = false;
+ double HostLatencySec = -1.0;
std::string CacheUrl;
std::string CacheName;
bool CacheAssumeHttp2 = false;
+ double CacheLatencySec = -1.0;
};
enum class ZenCacheResolveMode
@@ -54,9 +56,11 @@ struct StorageInstance
std::unique_ptr<HttpClient> BuildStorageHttp;
std::unique_ptr<BuildStorageBase> BuildStorage;
std::string StorageName;
+ double BuildStorageLatencySec = -1.0;
std::unique_ptr<HttpClient> CacheHttp;
std::unique_ptr<BuildStorageCache> BuildCacheStorage;
std::string CacheName;
+ double CacheLatencySec = -1.0;
};
} // namespace zen
diff --git a/src/zenremotestore/include/zenremotestore/chunking/chunkblock.h b/src/zenremotestore/include/zenremotestore/chunking/chunkblock.h
index d339b0f94..5a17ef79c 100644
--- a/src/zenremotestore/include/zenremotestore/chunking/chunkblock.h
+++ b/src/zenremotestore/include/zenremotestore/chunking/chunkblock.h
@@ -7,6 +7,10 @@
#include <zencore/compactbinary.h>
#include <zencore/compress.h>
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <tsl/robin_map.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
#include <optional>
#include <vector>
@@ -73,6 +77,101 @@ std::vector<size_t> FindReuseBlocks(OperationLogOutput& Output,
std::span<const uint32_t> ChunkIndexes,
std::vector<uint32_t>& OutUnusedChunkIndexes);
+class ChunkBlockAnalyser
+{
+public:
+ struct Options
+ {
+ bool IsQuiet = false;
+ bool IsVerbose = false;
+ double HostLatencySec = -1.0;
+ double HostHighSpeedLatencySec = -1.0;
+ uint64_t HostSpeedBytesPerSec = (1u * 1024u * 1024u * 1024u) / 8u; // 1GBit
+ uint64_t HostHighSpeedBytesPerSec = (2u * 1024u * 1024u * 1024u) / 8u; // 2GBit
+ };
+
+ ChunkBlockAnalyser(OperationLogOutput& LogOutput, std::span<const ChunkBlockDescription> BlockDescriptions, const Options& Options);
+
+ struct BlockRangeDescriptor
+ {
+ uint32_t BlockIndex = (uint32_t)-1;
+ uint64_t RangeStart = 0;
+ uint64_t RangeLength = 0;
+ uint32_t ChunkBlockIndexStart = 0;
+ uint32_t ChunkBlockIndexCount = 0;
+ };
+
+ struct NeededBlock
+ {
+ uint32_t BlockIndex;
+ std::vector<uint32_t> ChunkIndexes;
+ };
+
+ std::vector<NeededBlock> GetNeeded(const tsl::robin_map<IoHash, uint32_t, IoHash::Hasher>& ChunkHashToChunkIndex,
+ std::function<bool(uint32_t ChunkIndex)>&& NeedsBlockChunk);
+
+ enum EPartialBlockDownloadMode
+ {
+ Off,
+ SingleRange,
+ MultiRange,
+ MultiRangeHighSpeed,
+ Exact
+ };
+
+ struct BlockResult
+ {
+ std::vector<BlockRangeDescriptor> BlockRanges;
+ std::vector<uint32_t> FullBlockIndexes;
+ };
+
+ BlockResult CalculatePartialBlockDownloads(std::span<const NeededBlock> NeededBlocks,
+ std::span<const EPartialBlockDownloadMode> BlockPartialDownloadModes);
+
+private:
+ struct BlockRangeLimit
+ {
+ uint16_t SizePercent;
+ uint16_t MaxRangeCount;
+ };
+
+ static constexpr uint16_t FullBlockRangePercentLimit = 98;
+
+ static constexpr BlockRangeLimit ForceMergeLimits[] = {{.SizePercent = FullBlockRangePercentLimit, .MaxRangeCount = 1},
+ {.SizePercent = 90, .MaxRangeCount = 4},
+ {.SizePercent = 85, .MaxRangeCount = 16},
+ {.SizePercent = 80, .MaxRangeCount = 32},
+ {.SizePercent = 75, .MaxRangeCount = 48},
+ {.SizePercent = 70, .MaxRangeCount = 64},
+ {.SizePercent = 4, .MaxRangeCount = 82},
+ {.SizePercent = 0, .MaxRangeCount = 96}};
+
+ BlockRangeDescriptor MergeBlockRanges(std::span<const BlockRangeDescriptor> Ranges);
+ std::optional<std::vector<BlockRangeDescriptor>> MakeOptionalBlockRangeVector(uint64_t TotalBlockSize,
+ const BlockRangeDescriptor& Range);
+ const BlockRangeLimit* GetBlockRangeLimitForRange(std::span<const BlockRangeLimit> Limits,
+ uint64_t TotalBlockSize,
+ std::span<const BlockRangeDescriptor> Ranges);
+ std::vector<BlockRangeDescriptor> CollapseBlockRanges(const uint64_t AlwaysAcceptableGap,
+ std::span<const BlockRangeDescriptor> BlockRanges);
+ uint64_t CalculateNextGap(const uint64_t AlwaysAcceptableGap, std::span<const BlockRangeDescriptor> BlockRanges);
+ std::optional<std::vector<BlockRangeDescriptor>> CalculateBlockRanges(uint32_t BlockIndex,
+ const ChunkBlockDescription& BlockDescription,
+ std::span<const uint32_t> BlockChunkIndexNeeded,
+ EPartialBlockDownloadMode PartialBlockDownloadMode,
+ const uint64_t ChunkStartOffsetInBlock,
+ const uint64_t TotalBlockSize,
+ uint64_t& OutTotalWantedChunksSize);
+
+ OperationLogOutput& m_LogOutput;
+ const std::span<const ChunkBlockDescription> m_BlockDescriptions;
+ const Options m_Options;
+};
+
+#if ZEN_WITH_TESTS
+
+class CbWriter;
void chunkblock_forcelink();
+#endif // ZEN_WITH_TESTS
} // namespace zen
diff --git a/src/zenremotestore/include/zenremotestore/jupiter/jupiterhost.h b/src/zenremotestore/include/zenremotestore/jupiter/jupiterhost.h
index 432496bc1..7bbf40dfa 100644
--- a/src/zenremotestore/include/zenremotestore/jupiter/jupiterhost.h
+++ b/src/zenremotestore/include/zenremotestore/jupiter/jupiterhost.h
@@ -28,6 +28,7 @@ struct JupiterEndpointTestResult
{
bool Success = false;
std::string FailureReason;
+ double LatencySeconds = -1.0;
};
JupiterEndpointTestResult TestJupiterEndpoint(std::string_view BaseUrl, const bool AssumeHttp2, const bool HttpVerbose);
diff --git a/src/zenremotestore/include/zenremotestore/operationlogoutput.h b/src/zenremotestore/include/zenremotestore/operationlogoutput.h
index 9693e69cf..6f10ab156 100644
--- a/src/zenremotestore/include/zenremotestore/operationlogoutput.h
+++ b/src/zenremotestore/include/zenremotestore/operationlogoutput.h
@@ -3,6 +3,7 @@
#pragma once
#include <zencore/fmtutils.h>
+#include <zencore/logbase.h>
namespace zen {
@@ -57,9 +58,7 @@ public:
virtual ProgressBar* CreateProgressBar(std::string_view InSubTask) = 0;
};
-struct LoggerRef;
-
-OperationLogOutput* CreateStandardLogOutput(LoggerRef& Log);
+OperationLogOutput* CreateStandardLogOutput(LoggerRef Log);
#define ZEN_OPERATION_LOG(OutputTarget, InLevel, fmtstr, ...) \
do \
diff --git a/src/zenremotestore/include/zenremotestore/partialblockrequestmode.h b/src/zenremotestore/include/zenremotestore/partialblockrequestmode.h
new file mode 100644
index 000000000..54adea2b2
--- /dev/null
+++ b/src/zenremotestore/include/zenremotestore/partialblockrequestmode.h
@@ -0,0 +1,20 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <string_view>
+
+namespace zen {
+
+enum EPartialBlockRequestMode
+{
+ Off,
+ ZenCacheOnly,
+ Mixed,
+ All,
+ Invalid
+};
+
+EPartialBlockRequestMode PartialBlockRequestModeFromString(const std::string_view ModeString);
+
+} // namespace zen
diff --git a/src/zenremotestore/include/zenremotestore/projectstore/buildsremoteprojectstore.h b/src/zenremotestore/include/zenremotestore/projectstore/buildsremoteprojectstore.h
index e8b7c15c0..66dfcc62d 100644
--- a/src/zenremotestore/include/zenremotestore/projectstore/buildsremoteprojectstore.h
+++ b/src/zenremotestore/include/zenremotestore/projectstore/buildsremoteprojectstore.h
@@ -34,6 +34,8 @@ std::shared_ptr<RemoteProjectStore> CreateJupiterBuildsRemoteStore(LoggerRef
bool Quiet,
bool Unattended,
bool Hidden,
- WorkerThreadPool& CacheBackgroundWorkerPool);
+ WorkerThreadPool& CacheBackgroundWorkerPool,
+ double& OutHostLatencySec,
+ double& OutCacheLatencySec);
} // namespace zen
diff --git a/src/zenremotestore/include/zenremotestore/projectstore/remoteprojectstore.h b/src/zenremotestore/include/zenremotestore/projectstore/remoteprojectstore.h
index 008f94351..152c02ee2 100644
--- a/src/zenremotestore/include/zenremotestore/projectstore/remoteprojectstore.h
+++ b/src/zenremotestore/include/zenremotestore/projectstore/remoteprojectstore.h
@@ -6,6 +6,7 @@
#include <zenstore/projectstore.h>
#include <zenremotestore/chunking/chunkblock.h>
+#include <zenremotestore/partialblockrequestmode.h>
#include <unordered_set>
@@ -73,6 +74,16 @@ public:
std::vector<ChunkBlockDescription> Blocks;
};
+ struct GetBlockDescriptionsResult : public Result
+ {
+ std::vector<ChunkBlockDescription> Blocks;
+ };
+
+ struct AttachmentExistsInCacheResult : public Result
+ {
+ std::vector<bool> HasBody;
+ };
+
struct RemoteStoreInfo
{
bool CreateBlocks;
@@ -111,10 +122,20 @@ public:
virtual FinalizeResult FinalizeContainer(const IoHash& RawHash) = 0;
virtual SaveAttachmentsResult SaveAttachments(const std::vector<SharedBuffer>& Payloads) = 0;
- virtual LoadContainerResult LoadContainer() = 0;
- virtual GetKnownBlocksResult GetKnownBlocks() = 0;
- virtual LoadAttachmentResult LoadAttachment(const IoHash& RawHash) = 0;
- virtual LoadAttachmentsResult LoadAttachments(const std::vector<IoHash>& RawHashes) = 0;
+ virtual LoadContainerResult LoadContainer() = 0;
+ virtual GetKnownBlocksResult GetKnownBlocks() = 0;
+ virtual GetBlockDescriptionsResult GetBlockDescriptions(std::span<const IoHash> BlockHashes) = 0;
+ virtual AttachmentExistsInCacheResult AttachmentExistsInCache(std::span<const IoHash> RawHashes) = 0;
+
+ struct AttachmentRange
+ {
+ uint64_t Offset = 0;
+ uint64_t Bytes = (uint64_t)-1;
+
+ inline operator bool() const { return Offset != 0 || Bytes != (uint64_t)-1; }
+ };
+ virtual LoadAttachmentResult LoadAttachment(const IoHash& RawHash, const AttachmentRange& Range) = 0;
+ virtual LoadAttachmentsResult LoadAttachments(const std::vector<IoHash>& RawHashes) = 0;
virtual void Flush() = 0;
};
@@ -153,14 +174,15 @@ RemoteProjectStore::LoadContainerResult BuildContainer(
class JobContext;
-RemoteProjectStore::Result SaveOplogContainer(ProjectStore::Oplog& Oplog,
- const CbObject& ContainerObject,
- const std::function<void(std::span<IoHash> RawHashes)>& OnReferencedAttachments,
- const std::function<bool(const IoHash& RawHash)>& HasAttachment,
- const std::function<void(const IoHash& BlockHash, std::vector<IoHash>&& Chunks)>& OnNeedBlock,
- const std::function<void(const IoHash& RawHash)>& OnNeedAttachment,
- const std::function<void(const ChunkedInfo& Chunked)>& OnChunkedAttachment,
- JobContext* OptionalContext);
+RemoteProjectStore::Result SaveOplogContainer(
+ ProjectStore::Oplog& Oplog,
+ const CbObject& ContainerObject,
+ const std::function<void(std::span<IoHash> RawHashes)>& OnReferencedAttachments,
+ const std::function<bool(const IoHash& RawHash)>& HasAttachment,
+ const std::function<void(ThinChunkBlockDescription&& ThinBlockDescription, std::vector<uint32_t>&& NeededChunkIndexes)>& OnNeedBlock,
+ const std::function<void(const IoHash& RawHash)>& OnNeedAttachment,
+ const std::function<void(const ChunkedInfo& Chunked)>& OnChunkedAttachment,
+ JobContext* OptionalContext);
RemoteProjectStore::Result SaveOplog(CidStore& ChunkStore,
RemoteProjectStore& RemoteStore,
@@ -177,15 +199,18 @@ RemoteProjectStore::Result SaveOplog(CidStore& ChunkStore,
bool IgnoreMissingAttachments,
JobContext* OptionalContext);
-RemoteProjectStore::Result LoadOplog(CidStore& ChunkStore,
- RemoteProjectStore& RemoteStore,
- ProjectStore::Oplog& Oplog,
- WorkerThreadPool& NetworkWorkerPool,
- WorkerThreadPool& WorkerPool,
- bool ForceDownload,
- bool IgnoreMissingAttachments,
- bool CleanOplog,
- JobContext* OptionalContext);
+RemoteProjectStore::Result LoadOplog(CidStore& ChunkStore,
+ RemoteProjectStore& RemoteStore,
+ ProjectStore::Oplog& Oplog,
+ WorkerThreadPool& NetworkWorkerPool,
+ WorkerThreadPool& WorkerPool,
+ bool ForceDownload,
+ bool IgnoreMissingAttachments,
+ bool CleanOplog,
+ EPartialBlockRequestMode PartialBlockRequestMode,
+ double HostLatencySec,
+ double CacheLatencySec,
+ JobContext* OptionalContext);
std::vector<IoHash> GetBlockHashesFromOplog(CbObjectView ContainerObject);
std::vector<ThinChunkBlockDescription> GetBlocksFromOplog(CbObjectView ContainerObject, std::span<const IoHash> IncludeBlockHashes);
diff --git a/src/zenremotestore/jupiter/jupiterhost.cpp b/src/zenremotestore/jupiter/jupiterhost.cpp
index 7706f00c2..2583cfc84 100644
--- a/src/zenremotestore/jupiter/jupiterhost.cpp
+++ b/src/zenremotestore/jupiter/jupiterhost.cpp
@@ -59,7 +59,13 @@ TestJupiterEndpoint(std::string_view BaseUrl, const bool AssumeHttp2, const bool
HttpClient::Response TestResponse = TestHttpClient.Get("/health/live");
if (TestResponse.IsSuccess())
{
- return {.Success = true};
+ LatencyTestResult LatencyResult = MeasureLatency(TestHttpClient, "/health/ready");
+
+ if (!LatencyResult.Success)
+ {
+ return {.Success = false, .FailureReason = LatencyResult.FailureReason};
+ }
+ return {.Success = true, .LatencySeconds = LatencyResult.LatencySeconds};
}
return {.Success = false, .FailureReason = TestResponse.ErrorMessage("")};
}
diff --git a/src/zenremotestore/operationlogoutput.cpp b/src/zenremotestore/operationlogoutput.cpp
index 0837ed716..7ed93c947 100644
--- a/src/zenremotestore/operationlogoutput.cpp
+++ b/src/zenremotestore/operationlogoutput.cpp
@@ -95,7 +95,7 @@ StandardLogOutputProgressBar::Finish()
}
OperationLogOutput*
-CreateStandardLogOutput(LoggerRef& Log)
+CreateStandardLogOutput(LoggerRef Log)
{
return new StandardLogOutput(Log);
}
diff --git a/src/zenremotestore/partialblockrequestmode.cpp b/src/zenremotestore/partialblockrequestmode.cpp
new file mode 100644
index 000000000..b3edf515b
--- /dev/null
+++ b/src/zenremotestore/partialblockrequestmode.cpp
@@ -0,0 +1,27 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zenremotestore/partialblockrequestmode.h>
+
+#include <zencore/string.h>
+
+namespace zen {
+
+EPartialBlockRequestMode
+PartialBlockRequestModeFromString(const std::string_view ModeString)
+{
+ switch (HashStringAsLowerDjb2(ModeString))
+ {
+ case HashStringDjb2("false"):
+ return EPartialBlockRequestMode::Off;
+ case HashStringDjb2("zencacheonly"):
+ return EPartialBlockRequestMode::ZenCacheOnly;
+ case HashStringDjb2("mixed"):
+ return EPartialBlockRequestMode::Mixed;
+ case HashStringDjb2("true"):
+ return EPartialBlockRequestMode::All;
+ default:
+ return EPartialBlockRequestMode::Invalid;
+ }
+}
+
+} // namespace zen
diff --git a/src/zenremotestore/projectstore/buildsremoteprojectstore.cpp b/src/zenremotestore/projectstore/buildsremoteprojectstore.cpp
index a8e883dde..c42373e4d 100644
--- a/src/zenremotestore/projectstore/buildsremoteprojectstore.cpp
+++ b/src/zenremotestore/projectstore/buildsremoteprojectstore.cpp
@@ -441,7 +441,7 @@ public:
catch (const HttpClientError& Ex)
{
Result.ErrorCode = MakeErrorCode(Ex);
- Result.Reason = fmt::format("Failed listing know blocks for {}/{}/{}/{}. Reason: '{}'",
+ Result.Reason = fmt::format("Failed listing known blocks for {}/{}/{}/{}. Reason: '{}'",
m_BuildStorageHttp.GetBaseUri(),
m_Namespace,
m_Bucket,
@@ -451,7 +451,7 @@ public:
catch (const std::exception& Ex)
{
Result.ErrorCode = gsl::narrow<int32_t>(HttpResponseCode::InternalServerError);
- Result.Reason = fmt::format("Failed listing know blocks for {}/{}/{}/{}. Reason: '{}'",
+ Result.Reason = fmt::format("Failed listing known blocks for {}/{}/{}/{}. Reason: '{}'",
m_BuildStorageHttp.GetBaseUri(),
m_Namespace,
m_Bucket,
@@ -462,7 +462,94 @@ public:
return Result;
}
- virtual LoadAttachmentResult LoadAttachment(const IoHash& RawHash) override
+ virtual GetBlockDescriptionsResult GetBlockDescriptions(std::span<const IoHash> BlockHashes) override
+ {
+ std::unique_ptr<OperationLogOutput> Output(CreateStandardLogOutput(Log()));
+
+ ZEN_ASSERT(m_OplogBuildPartId != Oid::Zero);
+
+ GetBlockDescriptionsResult Result;
+ Stopwatch Timer;
+ auto _ = MakeGuard([&Timer, &Result]() { Result.ElapsedSeconds = Timer.GetElapsedTimeUs() / 1000000.0; });
+
+ try
+ {
+ Result.Blocks = zen::GetBlockDescriptions(*Output,
+ *m_BuildStorage,
+ m_BuildCacheStorage.get(),
+ m_BuildId,
+ m_OplogBuildPartId,
+ BlockHashes,
+ /*AttemptFallback*/ false,
+ /*IsQuiet*/ false,
+ /*IsVerbose)*/ false);
+ }
+ catch (const HttpClientError& Ex)
+ {
+ Result.ErrorCode = MakeErrorCode(Ex);
+ Result.Reason = fmt::format("Failed listing known blocks for {}/{}/{}/{}. Reason: '{}'",
+ m_BuildStorageHttp.GetBaseUri(),
+ m_Namespace,
+ m_Bucket,
+ m_BuildId,
+ Ex.what());
+ }
+ catch (const std::exception& Ex)
+ {
+ Result.ErrorCode = gsl::narrow<int32_t>(HttpResponseCode::InternalServerError);
+ Result.Reason = fmt::format("Failed listing known blocks for {}/{}/{}/{}. Reason: '{}'",
+ m_BuildStorageHttp.GetBaseUri(),
+ m_Namespace,
+ m_Bucket,
+ m_BuildId,
+ Ex.what());
+ }
+ return Result;
+ }
+
+ virtual AttachmentExistsInCacheResult AttachmentExistsInCache(std::span<const IoHash> RawHashes) override
+ {
+ AttachmentExistsInCacheResult Result;
+ Stopwatch Timer;
+ auto _ = MakeGuard([&Timer, &Result]() { Result.ElapsedSeconds = Timer.GetElapsedTimeUs() / 1000000.0; });
+ try
+ {
+ const std::vector<BuildStorageCache::BlobExistsResult> CacheExistsResult =
+ m_BuildCacheStorage->BlobsExists(m_BuildId, RawHashes);
+
+ if (CacheExistsResult.size() == RawHashes.size())
+ {
+ Result.HasBody.reserve(CacheExistsResult.size());
+ for (size_t BlobIndex = 0; BlobIndex < CacheExistsResult.size(); BlobIndex++)
+ {
+ Result.HasBody.push_back(CacheExistsResult[BlobIndex].HasBody);
+ }
+ }
+ }
+ catch (const HttpClientError& Ex)
+ {
+ Result.ErrorCode = MakeErrorCode(Ex);
+ Result.Reason = fmt::format("Remote cache: Failed finding known blobs for {}/{}/{}/{}. Reason: '{}'",
+ m_BuildStorageHttp.GetBaseUri(),
+ m_Namespace,
+ m_Bucket,
+ m_BuildId,
+ Ex.what());
+ }
+ catch (const std::exception& Ex)
+ {
+ Result.ErrorCode = gsl::narrow<int32_t>(HttpResponseCode::InternalServerError);
+ Result.Reason = fmt::format("Remote cache: Failed finding known blobs for {}/{}/{}/{}. Reason: '{}'",
+ m_BuildStorageHttp.GetBaseUri(),
+ m_Namespace,
+ m_Bucket,
+ m_BuildId,
+ Ex.what());
+ }
+ return Result;
+ }
+
+ virtual LoadAttachmentResult LoadAttachment(const IoHash& RawHash, const AttachmentRange& Range) override
{
ZEN_ASSERT(m_OplogBuildPartId != Oid::Zero);
@@ -474,7 +561,7 @@ public:
{
if (m_BuildCacheStorage)
{
- IoBuffer CachedBlob = m_BuildCacheStorage->GetBuildBlob(m_BuildId, RawHash);
+ IoBuffer CachedBlob = m_BuildCacheStorage->GetBuildBlob(m_BuildId, RawHash, Range.Offset, Range.Bytes);
if (CachedBlob)
{
Result.Bytes = std::move(CachedBlob);
@@ -482,20 +569,23 @@ public:
}
if (!Result.Bytes)
{
- Result.Bytes = m_BuildStorage->GetBuildBlob(m_BuildId, RawHash);
+ Result.Bytes = m_BuildStorage->GetBuildBlob(m_BuildId, RawHash, Range.Offset, Range.Bytes);
if (m_BuildCacheStorage && Result.Bytes && m_PopulateCache)
{
- m_BuildCacheStorage->PutBuildBlob(m_BuildId,
- RawHash,
- Result.Bytes.GetContentType(),
- CompositeBuffer(SharedBuffer(Result.Bytes)));
+ if (!Range)
+ {
+ m_BuildCacheStorage->PutBuildBlob(m_BuildId,
+ RawHash,
+ Result.Bytes.GetContentType(),
+ CompositeBuffer(SharedBuffer(Result.Bytes)));
+ }
}
}
}
catch (const HttpClientError& Ex)
{
Result.ErrorCode = MakeErrorCode(Ex);
- Result.Reason = fmt::format("Failed listing know blocks for {}/{}/{}/{}. Reason: '{}'",
+ Result.Reason = fmt::format("Failed listing known blocks for {}/{}/{}/{}. Reason: '{}'",
m_BuildStorageHttp.GetBaseUri(),
m_Namespace,
m_Bucket,
@@ -505,7 +595,7 @@ public:
catch (const std::exception& Ex)
{
Result.ErrorCode = gsl::narrow<int32_t>(HttpResponseCode::InternalServerError);
- Result.Reason = fmt::format("Failed listing know blocks for {}/{}/{}/{}. Reason: '{}'",
+ Result.Reason = fmt::format("Failed listing known blocks for {}/{}/{}/{}. Reason: '{}'",
m_BuildStorageHttp.GetBaseUri(),
m_Namespace,
m_Bucket,
@@ -558,7 +648,7 @@ public:
for (const IoHash& Hash : AttachmentsLeftToFind)
{
- LoadAttachmentResult ChunkResult = LoadAttachment(Hash);
+ LoadAttachmentResult ChunkResult = LoadAttachment(Hash, {});
if (ChunkResult.ErrorCode)
{
return LoadAttachmentsResult{ChunkResult};
@@ -623,7 +713,9 @@ CreateJupiterBuildsRemoteStore(LoggerRef InLog,
bool Quiet,
bool Unattended,
bool Hidden,
- WorkerThreadPool& CacheBackgroundWorkerPool)
+ WorkerThreadPool& CacheBackgroundWorkerPool,
+ double& OutHostLatencySec,
+ double& OutCacheLatencySec)
{
std::string Host = Options.Host;
if (!Host.empty() && Host.find("://"sv) == std::string::npos)
@@ -727,6 +819,10 @@ CreateJupiterBuildsRemoteStore(LoggerRef InLog,
Options.ForceDisableBlocks,
Options.ForceDisableTempBlocks,
Options.PopulateCache);
+
+ OutHostLatencySec = ResolveRes.HostLatencySec;
+ OutCacheLatencySec = ResolveRes.CacheLatencySec;
+
return RemoteStore;
}
diff --git a/src/zenremotestore/projectstore/fileremoteprojectstore.cpp b/src/zenremotestore/projectstore/fileremoteprojectstore.cpp
index 3a67d3842..ec7fb7bbc 100644
--- a/src/zenremotestore/projectstore/fileremoteprojectstore.cpp
+++ b/src/zenremotestore/projectstore/fileremoteprojectstore.cpp
@@ -217,7 +217,18 @@ public:
return Result;
}
- virtual LoadAttachmentResult LoadAttachment(const IoHash& RawHash) override
+ virtual GetBlockDescriptionsResult GetBlockDescriptions(std::span<const IoHash> BlockHashes) override
+ {
+ ZEN_UNUSED(BlockHashes);
+ return GetBlockDescriptionsResult{Result{.ErrorCode = int(HttpResponseCode::NotFound)}};
+ }
+
+ virtual AttachmentExistsInCacheResult AttachmentExistsInCache(std::span<const IoHash> RawHashes) override
+ {
+ return AttachmentExistsInCacheResult{Result{.ErrorCode = 0}, std::vector<bool>(RawHashes.size(), false)};
+ }
+
+ virtual LoadAttachmentResult LoadAttachment(const IoHash& RawHash, const AttachmentRange& Range) override
{
Stopwatch Timer;
LoadAttachmentResult Result;
@@ -232,7 +243,14 @@ public:
{
BasicFile ChunkFile;
ChunkFile.Open(ChunkPath, BasicFile::Mode::kRead);
- Result.Bytes = ChunkFile.ReadAll();
+ if (Range)
+ {
+ Result.Bytes = ChunkFile.ReadRange(Range.Offset, Range.Bytes);
+ }
+ else
+ {
+ Result.Bytes = ChunkFile.ReadAll();
+ }
}
AddStats(0, Result.Bytes.GetSize(), Timer.GetElapsedTimeUs() * 1000);
Result.ElapsedSeconds = Timer.GetElapsedTimeMs() / 1000.0;
@@ -245,7 +263,7 @@ public:
LoadAttachmentsResult Result;
for (const IoHash& Hash : RawHashes)
{
- LoadAttachmentResult ChunkResult = LoadAttachment(Hash);
+ LoadAttachmentResult ChunkResult = LoadAttachment(Hash, {});
if (ChunkResult.ErrorCode)
{
ChunkResult.ElapsedSeconds = Timer.GetElapsedTimeMs() / 1000.0;
diff --git a/src/zenremotestore/projectstore/jupiterremoteprojectstore.cpp b/src/zenremotestore/projectstore/jupiterremoteprojectstore.cpp
index 462de2988..f8179831c 100644
--- a/src/zenremotestore/projectstore/jupiterremoteprojectstore.cpp
+++ b/src/zenremotestore/projectstore/jupiterremoteprojectstore.cpp
@@ -212,7 +212,18 @@ public:
return Result;
}
- virtual LoadAttachmentResult LoadAttachment(const IoHash& RawHash) override
+ virtual GetBlockDescriptionsResult GetBlockDescriptions(std::span<const IoHash> BlockHashes) override
+ {
+ ZEN_UNUSED(BlockHashes);
+ return GetBlockDescriptionsResult{Result{.ErrorCode = int(HttpResponseCode::NotFound)}};
+ }
+
+ virtual AttachmentExistsInCacheResult AttachmentExistsInCache(std::span<const IoHash> RawHashes) override
+ {
+ return AttachmentExistsInCacheResult{Result{.ErrorCode = 0}, std::vector<bool>(RawHashes.size(), false)};
+ }
+
+ virtual LoadAttachmentResult LoadAttachment(const IoHash& RawHash, const AttachmentRange& Range) override
{
JupiterSession Session(m_JupiterClient->Logger(), m_JupiterClient->Client(), m_AllowRedirect);
JupiterResult GetResult = Session.GetCompressedBlob(m_Namespace, RawHash, m_TempFilePath);
@@ -227,6 +238,10 @@ public:
RawHash,
Result.Reason);
}
+ if (!Result.ErrorCode && Range)
+ {
+ Result.Bytes = IoBuffer(Result.Bytes, Range.Offset, Range.Bytes);
+ }
return Result;
}
@@ -235,7 +250,7 @@ public:
LoadAttachmentsResult Result;
for (const IoHash& Hash : RawHashes)
{
- LoadAttachmentResult ChunkResult = LoadAttachment(Hash);
+ LoadAttachmentResult ChunkResult = LoadAttachment(Hash, {});
if (ChunkResult.ErrorCode)
{
return LoadAttachmentsResult{ChunkResult};
diff --git a/src/zenremotestore/projectstore/remoteprojectstore.cpp b/src/zenremotestore/projectstore/remoteprojectstore.cpp
index 8be8eb0df..2a9da6f58 100644
--- a/src/zenremotestore/projectstore/remoteprojectstore.cpp
+++ b/src/zenremotestore/projectstore/remoteprojectstore.cpp
@@ -14,6 +14,7 @@
#include <zencore/trace.h>
#include <zencore/workthreadpool.h>
#include <zenhttp/httpcommon.h>
+#include <zenremotestore/chunking/chunkedcontent.h>
#include <zenremotestore/chunking/chunkedfile.h>
#include <zenremotestore/operationlogoutput.h>
#include <zenstore/cidstore.h>
@@ -229,29 +230,60 @@ namespace remotestore_impl {
struct DownloadInfo
{
- uint64_t OplogSizeBytes = 0;
- std::atomic<uint64_t> AttachmentsDownloaded = 0;
- std::atomic<uint64_t> AttachmentBlocksDownloaded = 0;
- std::atomic<uint64_t> AttachmentBytesDownloaded = 0;
- std::atomic<uint64_t> AttachmentBlockBytesDownloaded = 0;
- std::atomic<uint64_t> AttachmentsStored = 0;
- std::atomic<uint64_t> AttachmentBytesStored = 0;
- std::atomic_size_t MissingAttachmentCount = 0;
+ uint64_t OplogSizeBytes = 0;
+ std::atomic<uint64_t> AttachmentsDownloaded = 0;
+ std::atomic<uint64_t> AttachmentBlocksDownloaded = 0;
+ std::atomic<uint64_t> AttachmentBlocksRangesDownloaded = 0;
+ std::atomic<uint64_t> AttachmentBytesDownloaded = 0;
+ std::atomic<uint64_t> AttachmentBlockBytesDownloaded = 0;
+ std::atomic<uint64_t> AttachmentBlockRangeBytesDownloaded = 0;
+ std::atomic<uint64_t> AttachmentsStored = 0;
+ std::atomic<uint64_t> AttachmentBytesStored = 0;
+ std::atomic_size_t MissingAttachmentCount = 0;
};
- void DownloadAndSaveBlockChunks(CidStore& ChunkStore,
- RemoteProjectStore& RemoteStore,
- bool IgnoreMissingAttachments,
- JobContext* OptionalContext,
- WorkerThreadPool& NetworkWorkerPool,
- WorkerThreadPool& WorkerPool,
- Latch& AttachmentsDownloadLatch,
- Latch& AttachmentsWriteLatch,
- AsyncRemoteResult& RemoteResult,
- DownloadInfo& Info,
- Stopwatch& LoadAttachmentsTimer,
- std::atomic_uint64_t& DownloadStartMS,
- const std::vector<IoHash>& Chunks)
+ class JobContextLogOutput : public OperationLogOutput
+ {
+ public:
+ JobContextLogOutput(JobContext* OptionalContext) : m_OptionalContext(OptionalContext) {}
+ virtual void EmitLogMessage(int LogLevel, std::string_view Format, fmt::format_args Args) override
+ {
+ ZEN_UNUSED(LogLevel);
+ if (m_OptionalContext)
+ {
+ fmt::basic_memory_buffer<char, 250> MessageBuffer;
+ fmt::vformat_to(fmt::appender(MessageBuffer), Format, Args);
+ remotestore_impl::ReportMessage(m_OptionalContext, std::string_view(MessageBuffer.data(), MessageBuffer.size()));
+ }
+ }
+
+ virtual void SetLogOperationName(std::string_view Name) override { ZEN_UNUSED(Name); }
+ virtual void SetLogOperationProgress(uint32_t StepIndex, uint32_t StepCount) override { ZEN_UNUSED(StepIndex, StepCount); }
+ virtual uint32_t GetProgressUpdateDelayMS() override { return 0; }
+ virtual ProgressBar* CreateProgressBar(std::string_view InSubTask) override
+ {
+ ZEN_UNUSED(InSubTask);
+ return nullptr;
+ }
+
+ private:
+ JobContext* m_OptionalContext;
+ };
+
+ void DownloadAndSaveBlockChunks(CidStore& ChunkStore,
+ RemoteProjectStore& RemoteStore,
+ bool IgnoreMissingAttachments,
+ JobContext* OptionalContext,
+ WorkerThreadPool& NetworkWorkerPool,
+ WorkerThreadPool& WorkerPool,
+ Latch& AttachmentsDownloadLatch,
+ Latch& AttachmentsWriteLatch,
+ AsyncRemoteResult& RemoteResult,
+ DownloadInfo& Info,
+ Stopwatch& LoadAttachmentsTimer,
+ std::atomic_uint64_t& DownloadStartMS,
+ ThinChunkBlockDescription&& ThinBlockDescription,
+ std::vector<uint32_t>&& NeededChunkIndexes)
{
AttachmentsDownloadLatch.AddCount(1);
NetworkWorkerPool.ScheduleWork(
@@ -261,7 +293,8 @@ namespace remotestore_impl {
&AttachmentsDownloadLatch,
&AttachmentsWriteLatch,
&RemoteResult,
- Chunks = Chunks,
+ ThinBlockDescription = std::move(ThinBlockDescription),
+ NeededChunkIndexes = std::move(NeededChunkIndexes),
&Info,
&LoadAttachmentsTimer,
&DownloadStartMS,
@@ -276,6 +309,13 @@ namespace remotestore_impl {
}
try
{
+ std::vector<IoHash> Chunks;
+ Chunks.reserve(NeededChunkIndexes.size());
+ for (uint32_t ChunkIndex : NeededChunkIndexes)
+ {
+ Chunks.push_back(ThinBlockDescription.ChunkRawHashes[ChunkIndex]);
+ }
+
uint64_t Unset = (std::uint64_t)-1;
DownloadStartMS.compare_exchange_strong(Unset, LoadAttachmentsTimer.GetElapsedTimeMs());
RemoteProjectStore::LoadAttachmentsResult Result = RemoteStore.LoadAttachments(Chunks);
@@ -293,7 +333,12 @@ namespace remotestore_impl {
}
return;
}
- Info.AttachmentsDownloaded.fetch_add(Chunks.size());
+ Info.AttachmentsDownloaded.fetch_add(Result.Chunks.size());
+ for (const auto& It : Result.Chunks)
+ {
+ uint64_t ChunkSize = It.second.GetCompressedSize();
+ Info.AttachmentBytesDownloaded.fetch_add(ChunkSize);
+ }
ZEN_INFO("Loaded {} bulk attachments in {}",
Chunks.size(),
NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000)));
@@ -320,8 +365,6 @@ namespace remotestore_impl {
for (const auto& It : Chunks)
{
- uint64_t ChunkSize = It.second.GetCompressedSize();
- Info.AttachmentBytesDownloaded.fetch_add(ChunkSize);
WriteAttachmentBuffers.push_back(It.second.GetCompressed().Flatten().AsIoBuffer());
WriteRawHashes.push_back(It.first);
}
@@ -350,28 +393,29 @@ namespace remotestore_impl {
catch (const std::exception& Ex)
{
RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError),
- fmt::format("Failed to bulk load {} attachments", Chunks.size()),
+ fmt::format("Failed to bulk load {} attachments", NeededChunkIndexes.size()),
Ex.what());
}
},
WorkerThreadPool::EMode::EnableBacklog);
};
- void DownloadAndSaveBlock(CidStore& ChunkStore,
- RemoteProjectStore& RemoteStore,
- bool IgnoreMissingAttachments,
- JobContext* OptionalContext,
- WorkerThreadPool& NetworkWorkerPool,
- WorkerThreadPool& WorkerPool,
- Latch& AttachmentsDownloadLatch,
- Latch& AttachmentsWriteLatch,
- AsyncRemoteResult& RemoteResult,
- DownloadInfo& Info,
- Stopwatch& LoadAttachmentsTimer,
- std::atomic_uint64_t& DownloadStartMS,
- const IoHash& BlockHash,
- const std::vector<IoHash>& Chunks,
- uint32_t RetriesLeft)
+ void DownloadAndSaveBlock(CidStore& ChunkStore,
+ RemoteProjectStore& RemoteStore,
+ bool IgnoreMissingAttachments,
+ JobContext* OptionalContext,
+ WorkerThreadPool& NetworkWorkerPool,
+ WorkerThreadPool& WorkerPool,
+ Latch& AttachmentsDownloadLatch,
+ Latch& AttachmentsWriteLatch,
+ AsyncRemoteResult& RemoteResult,
+ DownloadInfo& Info,
+ Stopwatch& LoadAttachmentsTimer,
+ std::atomic_uint64_t& DownloadStartMS,
+ const IoHash& BlockHash,
+ const tsl::robin_map<IoHash, uint32_t, IoHash::Hasher>& AllNeededPartialChunkHashesLookup,
+ std::span<std::atomic<bool>> ChunkDownloadedFlags,
+ uint32_t RetriesLeft)
{
AttachmentsDownloadLatch.AddCount(1);
NetworkWorkerPool.ScheduleWork(
@@ -381,7 +425,6 @@ namespace remotestore_impl {
&RemoteStore,
&NetworkWorkerPool,
&WorkerPool,
- BlockHash,
&RemoteResult,
&Info,
&LoadAttachmentsTimer,
@@ -389,7 +432,9 @@ namespace remotestore_impl {
IgnoreMissingAttachments,
OptionalContext,
RetriesLeft,
- Chunks = std::vector<IoHash>(Chunks)]() {
+ BlockHash = IoHash(BlockHash),
+ &AllNeededPartialChunkHashesLookup,
+ ChunkDownloadedFlags]() {
ZEN_TRACE_CPU("DownloadBlock");
auto _ = MakeGuard([&AttachmentsDownloadLatch] { AttachmentsDownloadLatch.CountDown(); });
@@ -401,7 +446,7 @@ namespace remotestore_impl {
{
uint64_t Unset = (std::uint64_t)-1;
DownloadStartMS.compare_exchange_strong(Unset, LoadAttachmentsTimer.GetElapsedTimeMs());
- RemoteProjectStore::LoadAttachmentResult BlockResult = RemoteStore.LoadAttachment(BlockHash);
+ RemoteProjectStore::LoadAttachmentResult BlockResult = RemoteStore.LoadAttachment(BlockHash, {});
if (BlockResult.ErrorCode)
{
ReportMessage(OptionalContext,
@@ -422,10 +467,10 @@ namespace remotestore_impl {
}
uint64_t BlockSize = BlockResult.Bytes.GetSize();
Info.AttachmentBlocksDownloaded.fetch_add(1);
- ZEN_INFO("Loaded block attachment '{}' in {} ({})",
- BlockHash,
- NiceTimeSpanMs(static_cast<uint64_t>(BlockResult.ElapsedSeconds * 1000)),
- NiceBytes(BlockSize));
+ ZEN_DEBUG("Loaded block attachment '{}' in {} ({})",
+ BlockHash,
+ NiceTimeSpanMs(static_cast<uint64_t>(BlockResult.ElapsedSeconds * 1000)),
+ NiceBytes(BlockSize));
Info.AttachmentBlockBytesDownloaded.fetch_add(BlockSize);
AttachmentsWriteLatch.AddCount(1);
@@ -436,7 +481,6 @@ namespace remotestore_impl {
&RemoteStore,
&NetworkWorkerPool,
&WorkerPool,
- BlockHash,
&RemoteResult,
&Info,
&LoadAttachmentsTimer,
@@ -444,8 +488,10 @@ namespace remotestore_impl {
IgnoreMissingAttachments,
OptionalContext,
RetriesLeft,
- Chunks = std::move(Chunks),
- Bytes = std::move(BlockResult.Bytes)]() {
+ BlockHash = IoHash(BlockHash),
+ &AllNeededPartialChunkHashesLookup,
+ ChunkDownloadedFlags,
+ Bytes = std::move(BlockResult.Bytes)]() {
auto _ = MakeGuard([&AttachmentsWriteLatch] { AttachmentsWriteLatch.CountDown(); });
if (RemoteResult.IsError())
{
@@ -454,9 +500,6 @@ namespace remotestore_impl {
try
{
ZEN_ASSERT(Bytes.Size() > 0);
- std::unordered_set<IoHash, IoHash::Hasher> WantedChunks;
- WantedChunks.reserve(Chunks.size());
- WantedChunks.insert(Chunks.begin(), Chunks.end());
std::vector<IoBuffer> WriteAttachmentBuffers;
std::vector<IoHash> WriteRawHashes;
@@ -485,7 +528,8 @@ namespace remotestore_impl {
LoadAttachmentsTimer,
DownloadStartMS,
BlockHash,
- std::move(Chunks),
+ AllNeededPartialChunkHashesLookup,
+ ChunkDownloadedFlags,
RetriesLeft - 1);
}
ReportMessage(
@@ -519,7 +563,8 @@ namespace remotestore_impl {
LoadAttachmentsTimer,
DownloadStartMS,
BlockHash,
- std::move(Chunks),
+ AllNeededPartialChunkHashesLookup,
+ ChunkDownloadedFlags,
RetriesLeft - 1);
}
ReportMessage(OptionalContext,
@@ -546,28 +591,36 @@ namespace remotestore_impl {
uint64_t BlockSize = BlockPayload.GetSize();
uint64_t BlockHeaderSize = 0;
- bool StoreChunksOK = IterateChunkBlock(
- BlockPayload.Flatten(),
- [&WantedChunks, &WriteAttachmentBuffers, &WriteRawHashes, &Info, &PotentialSize](
- CompressedBuffer&& Chunk,
- const IoHash& AttachmentRawHash) {
- if (WantedChunks.contains(AttachmentRawHash))
- {
- WriteAttachmentBuffers.emplace_back(Chunk.GetCompressed().Flatten().AsIoBuffer());
- IoHash RawHash;
- uint64_t RawSize;
- ZEN_ASSERT(
- CompressedBuffer::ValidateCompressedHeader(WriteAttachmentBuffers.back(),
- RawHash,
- RawSize,
- /*OutOptionalTotalCompressedSize*/ nullptr));
- ZEN_ASSERT(RawHash == AttachmentRawHash);
- WriteRawHashes.emplace_back(AttachmentRawHash);
- WantedChunks.erase(AttachmentRawHash);
- PotentialSize += WriteAttachmentBuffers.back().GetSize();
- }
- },
- BlockHeaderSize);
+
+ bool StoreChunksOK = IterateChunkBlock(
+ BlockPayload.Flatten(),
+ [&AllNeededPartialChunkHashesLookup,
+ &ChunkDownloadedFlags,
+ &WriteAttachmentBuffers,
+ &WriteRawHashes,
+ &Info,
+ &PotentialSize](CompressedBuffer&& Chunk, const IoHash& AttachmentRawHash) {
+ auto ChunkIndexIt = AllNeededPartialChunkHashesLookup.find(AttachmentRawHash);
+ if (ChunkIndexIt != AllNeededPartialChunkHashesLookup.end())
+ {
+ bool Expected = false;
+ if (ChunkDownloadedFlags[ChunkIndexIt->second].compare_exchange_strong(Expected, true))
+ {
+ WriteAttachmentBuffers.emplace_back(Chunk.GetCompressed().Flatten().AsIoBuffer());
+ IoHash RawHash;
+ uint64_t RawSize;
+ ZEN_ASSERT(
+ CompressedBuffer::ValidateCompressedHeader(WriteAttachmentBuffers.back(),
+ RawHash,
+ RawSize,
+ /*OutOptionalTotalCompressedSize*/ nullptr));
+ ZEN_ASSERT(RawHash == AttachmentRawHash);
+ WriteRawHashes.emplace_back(AttachmentRawHash);
+ PotentialSize += WriteAttachmentBuffers.back().GetSize();
+ }
+ }
+ },
+ BlockHeaderSize);
if (!StoreChunksOK)
{
@@ -582,8 +635,6 @@ namespace remotestore_impl {
return;
}
- ZEN_ASSERT(WantedChunks.empty());
-
if (!WriteAttachmentBuffers.empty())
{
auto Results = ChunkStore.AddChunks(WriteAttachmentBuffers, WriteRawHashes);
@@ -625,6 +676,293 @@ namespace remotestore_impl {
WorkerThreadPool::EMode::EnableBacklog);
};
+ void DownloadAndSavePartialBlock(CidStore& ChunkStore,
+ RemoteProjectStore& RemoteStore,
+ bool IgnoreMissingAttachments,
+ JobContext* OptionalContext,
+ WorkerThreadPool& NetworkWorkerPool,
+ WorkerThreadPool& WorkerPool,
+ Latch& AttachmentsDownloadLatch,
+ Latch& AttachmentsWriteLatch,
+ AsyncRemoteResult& RemoteResult,
+ DownloadInfo& Info,
+ Stopwatch& LoadAttachmentsTimer,
+ std::atomic_uint64_t& DownloadStartMS,
+ const ChunkBlockDescription& BlockDescription,
+ std::span<const ChunkBlockAnalyser::BlockRangeDescriptor> BlockRangeDescriptors,
+ size_t BlockRangeIndexStart,
+ size_t BlockRangeCount,
+ const tsl::robin_map<IoHash, uint32_t, IoHash::Hasher>& AllNeededPartialChunkHashesLookup,
+ std::span<std::atomic<bool>> ChunkDownloadedFlags,
+ uint32_t RetriesLeft)
+ {
+ AttachmentsDownloadLatch.AddCount(1);
+ NetworkWorkerPool.ScheduleWork(
+ [&AttachmentsDownloadLatch,
+ &AttachmentsWriteLatch,
+ &ChunkStore,
+ &RemoteStore,
+ &NetworkWorkerPool,
+ &WorkerPool,
+ &RemoteResult,
+ &Info,
+ &LoadAttachmentsTimer,
+ &DownloadStartMS,
+ IgnoreMissingAttachments,
+ OptionalContext,
+ RetriesLeft,
+ BlockDescription,
+ BlockRangeDescriptors,
+ BlockRangeIndexStart,
+ BlockRangeCount,
+ &AllNeededPartialChunkHashesLookup,
+ ChunkDownloadedFlags]() {
+ ZEN_TRACE_CPU("DownloadBlockRanges");
+
+ auto _ = MakeGuard([&AttachmentsDownloadLatch] { AttachmentsDownloadLatch.CountDown(); });
+ try
+ {
+ uint64_t Unset = (std::uint64_t)-1;
+ DownloadStartMS.compare_exchange_strong(Unset, LoadAttachmentsTimer.GetElapsedTimeMs());
+
+ double DownloadElapsedSeconds = 0;
+ uint64_t DownloadedBytes = 0;
+
+ for (size_t BlockRangeIndex = BlockRangeIndexStart; BlockRangeIndex < BlockRangeIndexStart + BlockRangeCount;
+ BlockRangeIndex++)
+ {
+ if (RemoteResult.IsError())
+ {
+ return;
+ }
+
+ const ChunkBlockAnalyser::BlockRangeDescriptor& BlockRange = BlockRangeDescriptors[BlockRangeIndex];
+
+ RemoteProjectStore::LoadAttachmentResult BlockResult =
+ RemoteStore.LoadAttachment(BlockDescription.BlockHash,
+ {.Offset = BlockRange.RangeStart, .Bytes = BlockRange.RangeLength});
+ if (BlockResult.ErrorCode)
+ {
+ ReportMessage(OptionalContext,
+ fmt::format("Failed to download block attachment '{}' range {},{} ({}): {}",
+ BlockDescription.BlockHash,
+ BlockRange.RangeStart,
+ BlockRange.RangeLength,
+ BlockResult.ErrorCode,
+ BlockResult.Reason));
+ Info.MissingAttachmentCount.fetch_add(1);
+ if (!IgnoreMissingAttachments)
+ {
+ RemoteResult.SetError(BlockResult.ErrorCode, BlockResult.Reason, BlockResult.Text);
+ }
+ return;
+ }
+ if (RemoteResult.IsError())
+ {
+ return;
+ }
+ uint64_t BlockPartSize = BlockResult.Bytes.GetSize();
+ if (BlockPartSize != BlockRange.RangeLength)
+ {
+ std::string ErrorString =
+ fmt::format("Failed to download block attachment '{}' range {},{}, got {} bytes ({}): {}",
+ BlockDescription.BlockHash,
+ BlockRange.RangeStart,
+ BlockRange.RangeLength,
+ BlockPartSize,
+ RemoteResult.GetError(),
+ RemoteResult.GetErrorReason());
+
+ ReportMessage(OptionalContext, ErrorString);
+ Info.MissingAttachmentCount.fetch_add(1);
+ if (!IgnoreMissingAttachments)
+ {
+ RemoteResult.SetError(gsl::narrow<int32_t>(HttpResponseCode::NotFound),
+ "Mismatching block part range received",
+ ErrorString);
+ }
+ return;
+ }
+ Info.AttachmentBlocksRangesDownloaded.fetch_add(1);
+
+ DownloadElapsedSeconds += BlockResult.ElapsedSeconds;
+ DownloadedBytes += BlockPartSize;
+
+ Info.AttachmentBlockRangeBytesDownloaded.fetch_add(BlockPartSize);
+
+ AttachmentsWriteLatch.AddCount(1);
+ WorkerPool.ScheduleWork(
+ [&AttachmentsDownloadLatch,
+ &AttachmentsWriteLatch,
+ &ChunkStore,
+ &RemoteStore,
+ &NetworkWorkerPool,
+ &WorkerPool,
+ &RemoteResult,
+ &Info,
+ &LoadAttachmentsTimer,
+ &DownloadStartMS,
+ IgnoreMissingAttachments,
+ OptionalContext,
+ RetriesLeft,
+ BlockDescription,
+ BlockRange,
+ &AllNeededPartialChunkHashesLookup,
+ ChunkDownloadedFlags,
+ BlockPayload = std::move(BlockResult.Bytes)]() {
+ auto _ = MakeGuard([&AttachmentsWriteLatch] { AttachmentsWriteLatch.CountDown(); });
+ if (RemoteResult.IsError())
+ {
+ return;
+ }
+ try
+ {
+ ZEN_ASSERT(BlockPayload.Size() > 0);
+ std::vector<IoBuffer> WriteAttachmentBuffers;
+ std::vector<IoHash> WriteRawHashes;
+
+ uint64_t PotentialSize = 0;
+ uint64_t UsedSize = 0;
+ uint64_t BlockPartSize = BlockPayload.GetSize();
+
+ uint32_t OffsetInBlock = 0;
+ for (uint32_t ChunkBlockIndex = BlockRange.ChunkBlockIndexStart;
+ ChunkBlockIndex < BlockRange.ChunkBlockIndexStart + BlockRange.ChunkBlockIndexCount;
+ ChunkBlockIndex++)
+ {
+ const uint32_t ChunkCompressedSize = BlockDescription.ChunkCompressedLengths[ChunkBlockIndex];
+ const IoHash& ChunkHash = BlockDescription.ChunkRawHashes[ChunkBlockIndex];
+
+ if (auto ChunkIndexIt = AllNeededPartialChunkHashesLookup.find(ChunkHash);
+ ChunkIndexIt != AllNeededPartialChunkHashesLookup.end())
+ {
+ bool Expected = false;
+ if (ChunkDownloadedFlags[ChunkIndexIt->second].compare_exchange_strong(Expected, true))
+ {
+ IoHash VerifyChunkHash;
+ uint64_t VerifyChunkSize;
+ CompressedBuffer CompressedChunk = CompressedBuffer::FromCompressed(
+ SharedBuffer(IoBuffer(BlockPayload, OffsetInBlock, ChunkCompressedSize)),
+ VerifyChunkHash,
+ VerifyChunkSize);
+ if (!CompressedChunk)
+ {
+ std::string ErrorString = fmt::format(
+ "Chunk at {},{} in block attachment '{}' is not a valid compressed buffer",
+ OffsetInBlock,
+ ChunkCompressedSize,
+ BlockDescription.BlockHash);
+ ReportMessage(OptionalContext, ErrorString);
+ Info.MissingAttachmentCount.fetch_add(1);
+ if (!IgnoreMissingAttachments)
+ {
+ RemoteResult.SetError(gsl::narrow<int32_t>(HttpResponseCode::NotFound),
+ "Malformed chunk block",
+ ErrorString);
+ }
+ continue;
+ }
+ if (VerifyChunkHash != ChunkHash)
+ {
+ std::string ErrorString = fmt::format(
+ "Chunk at {},{} in block attachment '{}' has mismatching hash, expected {}, got {}",
+ OffsetInBlock,
+ ChunkCompressedSize,
+ BlockDescription.BlockHash,
+ ChunkHash,
+ VerifyChunkHash);
+ ReportMessage(OptionalContext, ErrorString);
+ Info.MissingAttachmentCount.fetch_add(1);
+ if (!IgnoreMissingAttachments)
+ {
+ RemoteResult.SetError(gsl::narrow<int32_t>(HttpResponseCode::NotFound),
+ "Malformed chunk block",
+ ErrorString);
+ }
+ continue;
+ }
+ if (VerifyChunkSize != BlockDescription.ChunkRawLengths[ChunkBlockIndex])
+ {
+ std::string ErrorString = fmt::format(
+ "Chunk at {},{} in block attachment '{}' has mismatching raw size, expected {}, "
+ "got {}",
+ OffsetInBlock,
+ ChunkCompressedSize,
+ BlockDescription.BlockHash,
+ BlockDescription.ChunkRawLengths[ChunkBlockIndex],
+ VerifyChunkSize);
+ ReportMessage(OptionalContext, ErrorString);
+ Info.MissingAttachmentCount.fetch_add(1);
+ if (!IgnoreMissingAttachments)
+ {
+ RemoteResult.SetError(gsl::narrow<int32_t>(HttpResponseCode::NotFound),
+ "Malformed chunk block",
+ ErrorString);
+ }
+ continue;
+ }
+
+ WriteAttachmentBuffers.emplace_back(CompressedChunk.GetCompressed().Flatten().AsIoBuffer());
+ WriteRawHashes.emplace_back(ChunkHash);
+ PotentialSize += WriteAttachmentBuffers.back().GetSize();
+ }
+ }
+ OffsetInBlock += ChunkCompressedSize;
+ }
+
+ if (!WriteAttachmentBuffers.empty())
+ {
+ auto Results = ChunkStore.AddChunks(WriteAttachmentBuffers, WriteRawHashes);
+ for (size_t Index = 0; Index < Results.size(); Index++)
+ {
+ const auto& Result = Results[Index];
+ if (Result.New)
+ {
+ Info.AttachmentBytesStored.fetch_add(WriteAttachmentBuffers[Index].GetSize());
+ Info.AttachmentsStored.fetch_add(1);
+ UsedSize += WriteAttachmentBuffers[Index].GetSize();
+ }
+ }
+ ZEN_DEBUG("Used {} (matching {}) out of {} for block {} range {}, {} ({} %) (use of matching {}%)",
+ NiceBytes(UsedSize),
+ NiceBytes(PotentialSize),
+ NiceBytes(BlockPartSize),
+ BlockDescription.BlockHash,
+ BlockRange.RangeStart,
+ BlockRange.RangeLength,
+ (100 * UsedSize) / BlockPartSize,
+ PotentialSize > 0 ? (UsedSize * 100) / PotentialSize : 0);
+ }
+ }
+ catch (const std::exception& Ex)
+ {
+ RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError),
+ fmt::format("Failed save block attachment {} range {}, {}",
+ BlockDescription.BlockHash,
+ BlockRange.RangeStart,
+ BlockRange.RangeLength),
+ Ex.what());
+ }
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+ }
+
+ ZEN_DEBUG("Loaded {} ranges from block attachment '{}' in {} ({})",
+ BlockRangeCount,
+ BlockDescription.BlockHash,
+ NiceTimeSpanMs(static_cast<uint64_t>(DownloadElapsedSeconds * 1000)),
+ NiceBytes(DownloadedBytes));
+ }
+ catch (const std::exception& Ex)
+ {
+ RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError),
+ fmt::format("Failed to download block attachment {} ranges", BlockDescription.BlockHash),
+ Ex.what());
+ }
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+ };
+
void DownloadAndSaveAttachment(CidStore& ChunkStore,
RemoteProjectStore& RemoteStore,
bool IgnoreMissingAttachments,
@@ -664,7 +1002,7 @@ namespace remotestore_impl {
{
uint64_t Unset = (std::uint64_t)-1;
DownloadStartMS.compare_exchange_strong(Unset, LoadAttachmentsTimer.GetElapsedTimeMs());
- RemoteProjectStore::LoadAttachmentResult AttachmentResult = RemoteStore.LoadAttachment(RawHash);
+ RemoteProjectStore::LoadAttachmentResult AttachmentResult = RemoteStore.LoadAttachment(RawHash, {});
if (AttachmentResult.ErrorCode)
{
ReportMessage(OptionalContext,
@@ -680,10 +1018,10 @@ namespace remotestore_impl {
return;
}
uint64_t AttachmentSize = AttachmentResult.Bytes.GetSize();
- ZEN_INFO("Loaded large attachment '{}' in {} ({})",
- RawHash,
- NiceTimeSpanMs(static_cast<uint64_t>(AttachmentResult.ElapsedSeconds * 1000)),
- NiceBytes(AttachmentSize));
+ ZEN_DEBUG("Loaded large attachment '{}' in {} ({})",
+ RawHash,
+ NiceTimeSpanMs(static_cast<uint64_t>(AttachmentResult.ElapsedSeconds * 1000)),
+ NiceBytes(AttachmentSize));
Info.AttachmentsDownloaded.fetch_add(1);
if (RemoteResult.IsError())
{
@@ -1224,35 +1562,7 @@ BuildContainer(CidStore& ChunkStore,
{
using namespace std::literals;
- class JobContextLogOutput : public OperationLogOutput
- {
- public:
- JobContextLogOutput(JobContext* OptionalContext) : m_OptionalContext(OptionalContext) {}
- virtual void EmitLogMessage(int LogLevel, std::string_view Format, fmt::format_args Args) override
- {
- ZEN_UNUSED(LogLevel);
- if (m_OptionalContext)
- {
- fmt::basic_memory_buffer<char, 250> MessageBuffer;
- fmt::vformat_to(fmt::appender(MessageBuffer), Format, Args);
- remotestore_impl::ReportMessage(m_OptionalContext, std::string_view(MessageBuffer.data(), MessageBuffer.size()));
- }
- }
-
- virtual void SetLogOperationName(std::string_view Name) override { ZEN_UNUSED(Name); }
- virtual void SetLogOperationProgress(uint32_t StepIndex, uint32_t StepCount) override { ZEN_UNUSED(StepIndex, StepCount); }
- virtual uint32_t GetProgressUpdateDelayMS() override { return 0; }
- virtual ProgressBar* CreateProgressBar(std::string_view InSubTask) override
- {
- ZEN_UNUSED(InSubTask);
- return nullptr;
- }
-
- private:
- JobContext* m_OptionalContext;
- };
-
- std::unique_ptr<OperationLogOutput> LogOutput(std::make_unique<JobContextLogOutput>(OptionalContext));
+ std::unique_ptr<OperationLogOutput> LogOutput(std::make_unique<remotestore_impl::JobContextLogOutput>(OptionalContext));
size_t OpCount = 0;
@@ -2768,14 +3078,15 @@ SaveOplog(CidStore& ChunkStore,
};
RemoteProjectStore::Result
-ParseOplogContainer(const CbObject& ContainerObject,
- const std::function<void(std::span<IoHash> RawHashes)>& OnReferencedAttachments,
- const std::function<bool(const IoHash& RawHash)>& HasAttachment,
- const std::function<void(const IoHash& BlockHash, std::vector<IoHash>&& Chunks)>& OnNeedBlock,
- const std::function<void(const IoHash& RawHash)>& OnNeedAttachment,
- const std::function<void(const ChunkedInfo&)>& OnChunkedAttachment,
- CbObject& OutOplogSection,
- JobContext* OptionalContext)
+ParseOplogContainer(
+ const CbObject& ContainerObject,
+ const std::function<void(std::span<IoHash> RawHashes)>& OnReferencedAttachments,
+ const std::function<bool(const IoHash& RawHash)>& HasAttachment,
+ const std::function<void(ThinChunkBlockDescription&& ThinBlockDescription, std::vector<uint32_t>&& NeededChunkIndexes)>& OnNeedBlock,
+ const std::function<void(const IoHash& RawHash)>& OnNeedAttachment,
+ const std::function<void(const ChunkedInfo&)>& OnChunkedAttachment,
+ CbObject& OutOplogSection,
+ JobContext* OptionalContext)
{
using namespace std::literals;
@@ -2801,12 +3112,12 @@ ParseOplogContainer(const CbObject& ContainerObject,
"Section has unexpected data type",
"Failed to save oplog container"};
}
- std::unordered_set<IoHash, IoHash::Hasher> OpsAttachments;
+ std::unordered_set<IoHash, IoHash::Hasher> NeededAttachments;
{
CbArrayView OpsArray = OutOplogSection["ops"sv].AsArrayView();
for (CbFieldView OpEntry : OpsArray)
{
- OpEntry.IterateAttachments([&](CbFieldView FieldView) { OpsAttachments.insert(FieldView.AsAttachment()); });
+ OpEntry.IterateAttachments([&](CbFieldView FieldView) { NeededAttachments.insert(FieldView.AsAttachment()); });
if (remotestore_impl::IsCancelled(OptionalContext))
{
return RemoteProjectStore::Result{.ErrorCode = gsl::narrow<int>(HttpResponseCode::OK),
@@ -2816,7 +3127,7 @@ ParseOplogContainer(const CbObject& ContainerObject,
}
}
{
- std::vector<IoHash> ReferencedAttachments(OpsAttachments.begin(), OpsAttachments.end());
+ std::vector<IoHash> ReferencedAttachments(NeededAttachments.begin(), NeededAttachments.end());
OnReferencedAttachments(ReferencedAttachments);
}
@@ -2827,24 +3138,27 @@ ParseOplogContainer(const CbObject& ContainerObject,
.Reason = "Operation cancelled"};
}
- remotestore_impl::ReportMessage(OptionalContext, fmt::format("Oplog references {} attachments", OpsAttachments.size()));
+ remotestore_impl::ReportMessage(OptionalContext, fmt::format("Oplog references {} attachments", NeededAttachments.size()));
CbArrayView ChunkedFilesArray = ContainerObject["chunkedfiles"sv].AsArrayView();
for (CbFieldView ChunkedFileField : ChunkedFilesArray)
{
CbObjectView ChunkedFileView = ChunkedFileField.AsObjectView();
IoHash RawHash = ChunkedFileView["rawhash"sv].AsHash();
- if (OpsAttachments.contains(RawHash) && (!HasAttachment(RawHash)))
+ if (NeededAttachments.erase(RawHash) == 1)
{
- ChunkedInfo Chunked = ReadChunkedInfo(ChunkedFileView);
-
- OnReferencedAttachments(Chunked.ChunkHashes);
- OpsAttachments.insert(Chunked.ChunkHashes.begin(), Chunked.ChunkHashes.end());
- OnChunkedAttachment(Chunked);
- ZEN_INFO("Requesting chunked attachment '{}' ({}) built from {} chunks",
- Chunked.RawHash,
- NiceBytes(Chunked.RawSize),
- Chunked.ChunkHashes.size());
+ if (!HasAttachment(RawHash))
+ {
+ ChunkedInfo Chunked = ReadChunkedInfo(ChunkedFileView);
+
+ OnReferencedAttachments(Chunked.ChunkHashes);
+ NeededAttachments.insert(Chunked.ChunkHashes.begin(), Chunked.ChunkHashes.end());
+ OnChunkedAttachment(Chunked);
+ ZEN_INFO("Requesting chunked attachment '{}' ({}) built from {} chunks",
+ Chunked.RawHash,
+ NiceBytes(Chunked.RawSize),
+ Chunked.ChunkHashes.size());
+ }
}
if (remotestore_impl::IsCancelled(OptionalContext))
{
@@ -2854,6 +3168,8 @@ ParseOplogContainer(const CbObject& ContainerObject,
}
}
+ std::vector<ThinChunkBlockDescription> ThinBlocksDescriptions;
+
size_t NeedBlockCount = 0;
CbArrayView BlocksArray = ContainerObject["blocks"sv].AsArrayView();
for (CbFieldView BlockField : BlocksArray)
@@ -2863,45 +3179,38 @@ ParseOplogContainer(const CbObject& ContainerObject,
CbArrayView ChunksArray = BlockView["chunks"sv].AsArrayView();
- std::vector<IoHash> NeededChunks;
- NeededChunks.reserve(ChunksArray.Num());
- if (BlockHash == IoHash::Zero)
+ std::vector<IoHash> ChunkHashes;
+ ChunkHashes.reserve(ChunksArray.Num());
+ for (CbFieldView ChunkField : ChunksArray)
{
- for (CbFieldView ChunkField : ChunksArray)
- {
- IoHash ChunkHash = ChunkField.AsBinaryAttachment();
- if (OpsAttachments.erase(ChunkHash) == 1)
- {
- if (!HasAttachment(ChunkHash))
- {
- NeededChunks.emplace_back(ChunkHash);
- }
- }
- }
+ ChunkHashes.push_back(ChunkField.AsHash());
}
- else
+ ThinBlocksDescriptions.push_back(ThinChunkBlockDescription{.BlockHash = BlockHash, .ChunkRawHashes = std::move(ChunkHashes)});
+ }
+
+ for (ThinChunkBlockDescription& ThinBlockDescription : ThinBlocksDescriptions)
+ {
+ std::vector<uint32_t> NeededBlockChunkIndexes;
+ for (uint32_t ChunkIndex = 0; ChunkIndex < ThinBlockDescription.ChunkRawHashes.size(); ChunkIndex++)
{
- for (CbFieldView ChunkField : ChunksArray)
+ const IoHash& ChunkHash = ThinBlockDescription.ChunkRawHashes[ChunkIndex];
+ if (NeededAttachments.erase(ChunkHash) == 1)
{
- const IoHash ChunkHash = ChunkField.AsHash();
- if (OpsAttachments.erase(ChunkHash) == 1)
+ if (!HasAttachment(ChunkHash))
{
- if (!HasAttachment(ChunkHash))
- {
- NeededChunks.emplace_back(ChunkHash);
- }
+ NeededBlockChunkIndexes.push_back(ChunkIndex);
}
}
}
-
- if (!NeededChunks.empty())
+ if (!NeededBlockChunkIndexes.empty())
{
- OnNeedBlock(BlockHash, std::move(NeededChunks));
- if (BlockHash != IoHash::Zero)
+ if (ThinBlockDescription.BlockHash != IoHash::Zero)
{
NeedBlockCount++;
}
+ OnNeedBlock(std::move(ThinBlockDescription), std::move(NeededBlockChunkIndexes));
}
+
if (remotestore_impl::IsCancelled(OptionalContext))
{
return RemoteProjectStore::Result{.ErrorCode = gsl::narrow<int>(HttpResponseCode::OK),
@@ -2909,6 +3218,7 @@ ParseOplogContainer(const CbObject& ContainerObject,
.Reason = "Operation cancelled"};
}
}
+
remotestore_impl::ReportMessage(OptionalContext,
fmt::format("Requesting {} of {} attachment blocks", NeedBlockCount, BlocksArray.Num()));
@@ -2918,7 +3228,7 @@ ParseOplogContainer(const CbObject& ContainerObject,
{
IoHash AttachmentHash = LargeChunksField.AsBinaryAttachment();
- if (OpsAttachments.erase(AttachmentHash) == 1)
+ if (NeededAttachments.erase(AttachmentHash) == 1)
{
if (!HasAttachment(AttachmentHash))
{
@@ -2941,14 +3251,15 @@ ParseOplogContainer(const CbObject& ContainerObject,
}
RemoteProjectStore::Result
-SaveOplogContainer(ProjectStore::Oplog& Oplog,
- const CbObject& ContainerObject,
- const std::function<void(std::span<IoHash> RawHashes)>& OnReferencedAttachments,
- const std::function<bool(const IoHash& RawHash)>& HasAttachment,
- const std::function<void(const IoHash& BlockHash, std::vector<IoHash>&& Chunks)>& OnNeedBlock,
- const std::function<void(const IoHash& RawHash)>& OnNeedAttachment,
- const std::function<void(const ChunkedInfo&)>& OnChunkedAttachment,
- JobContext* OptionalContext)
+SaveOplogContainer(
+ ProjectStore::Oplog& Oplog,
+ const CbObject& ContainerObject,
+ const std::function<void(std::span<IoHash> RawHashes)>& OnReferencedAttachments,
+ const std::function<bool(const IoHash& RawHash)>& HasAttachment,
+ const std::function<void(ThinChunkBlockDescription&& ThinBlockDescription, std::vector<uint32_t>&& NeededChunkIndexes)>& OnNeedBlock,
+ const std::function<void(const IoHash& RawHash)>& OnNeedAttachment,
+ const std::function<void(const ChunkedInfo&)>& OnChunkedAttachment,
+ JobContext* OptionalContext)
{
using namespace std::literals;
@@ -2972,18 +3283,23 @@ SaveOplogContainer(ProjectStore::Oplog& Oplog,
}
RemoteProjectStore::Result
-LoadOplog(CidStore& ChunkStore,
- RemoteProjectStore& RemoteStore,
- ProjectStore::Oplog& Oplog,
- WorkerThreadPool& NetworkWorkerPool,
- WorkerThreadPool& WorkerPool,
- bool ForceDownload,
- bool IgnoreMissingAttachments,
- bool CleanOplog,
- JobContext* OptionalContext)
+LoadOplog(CidStore& ChunkStore,
+ RemoteProjectStore& RemoteStore,
+ ProjectStore::Oplog& Oplog,
+ WorkerThreadPool& NetworkWorkerPool,
+ WorkerThreadPool& WorkerPool,
+ bool ForceDownload,
+ bool IgnoreMissingAttachments,
+ bool CleanOplog,
+ EPartialBlockRequestMode PartialBlockRequestMode,
+ double HostLatencySec,
+ double CacheLatencySec,
+ JobContext* OptionalContext)
{
using namespace std::literals;
+ std::unique_ptr<OperationLogOutput> LogOutput(std::make_unique<remotestore_impl::JobContextLogOutput>(OptionalContext));
+
remotestore_impl::DownloadInfo Info;
Stopwatch Timer;
@@ -3035,6 +3351,14 @@ LoadOplog(CidStore& ChunkStore,
return false;
};
+ struct NeededBlockDownload
+ {
+ ThinChunkBlockDescription ThinBlockDescription;
+ std::vector<uint32_t> NeededChunkIndexes;
+ };
+
+ std::vector<NeededBlockDownload> NeededBlockDownloads;
+
auto OnNeedBlock = [&RemoteStore,
&ChunkStore,
&NetworkWorkerPool,
@@ -3047,8 +3371,9 @@ LoadOplog(CidStore& ChunkStore,
&Info,
&LoadAttachmentsTimer,
&DownloadStartMS,
+ &NeededBlockDownloads,
IgnoreMissingAttachments,
- OptionalContext](const IoHash& BlockHash, std::vector<IoHash>&& Chunks) {
+ OptionalContext](ThinChunkBlockDescription&& ThinBlockDescription, std::vector<uint32_t>&& NeededChunkIndexes) {
if (RemoteResult.IsError())
{
return;
@@ -3056,7 +3381,7 @@ LoadOplog(CidStore& ChunkStore,
BlockCountToDownload++;
AttachmentCount.fetch_add(1);
- if (BlockHash == IoHash::Zero)
+ if (ThinBlockDescription.BlockHash == IoHash::Zero)
{
DownloadAndSaveBlockChunks(ChunkStore,
RemoteStore,
@@ -3070,25 +3395,13 @@ LoadOplog(CidStore& ChunkStore,
Info,
LoadAttachmentsTimer,
DownloadStartMS,
- Chunks);
+ std::move(ThinBlockDescription),
+ std::move(NeededChunkIndexes));
}
else
{
- DownloadAndSaveBlock(ChunkStore,
- RemoteStore,
- IgnoreMissingAttachments,
- OptionalContext,
- NetworkWorkerPool,
- WorkerPool,
- AttachmentsDownloadLatch,
- AttachmentsWriteLatch,
- RemoteResult,
- Info,
- LoadAttachmentsTimer,
- DownloadStartMS,
- BlockHash,
- Chunks,
- 3);
+ NeededBlockDownloads.push_back(NeededBlockDownload{.ThinBlockDescription = std::move(ThinBlockDescription),
+ .NeededChunkIndexes = std::move(NeededChunkIndexes)});
}
};
@@ -3132,12 +3445,7 @@ LoadOplog(CidStore& ChunkStore,
};
std::vector<ChunkedInfo> FilesToDechunk;
- auto OnChunkedAttachment = [&Oplog, &ChunkStore, &FilesToDechunk, ForceDownload](const ChunkedInfo& Chunked) {
- if (ForceDownload || !ChunkStore.ContainsChunk(Chunked.RawHash))
- {
- FilesToDechunk.push_back(Chunked);
- }
- };
+ auto OnChunkedAttachment = [&FilesToDechunk](const ChunkedInfo& Chunked) { FilesToDechunk.push_back(Chunked); };
auto OnReferencedAttachments = [&Oplog](std::span<IoHash> RawHashes) { Oplog.CaptureAddedAttachments(RawHashes); };
@@ -3165,6 +3473,185 @@ LoadOplog(CidStore& ChunkStore,
BlockCountToDownload,
FilesToDechunk.size()));
+ std::vector<IoHash> BlockHashes;
+ std::vector<IoHash> AllNeededChunkHashes;
+ BlockHashes.reserve(NeededBlockDownloads.size());
+ for (const NeededBlockDownload& BlockDownload : NeededBlockDownloads)
+ {
+ BlockHashes.push_back(BlockDownload.ThinBlockDescription.BlockHash);
+ for (uint32_t ChunkIndex : BlockDownload.NeededChunkIndexes)
+ {
+ AllNeededChunkHashes.push_back(BlockDownload.ThinBlockDescription.ChunkRawHashes[ChunkIndex]);
+ }
+ }
+
+ tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> AllNeededPartialChunkHashesLookup = BuildHashLookup(AllNeededChunkHashes);
+ std::vector<std::atomic<bool>> ChunkDownloadedFlags(AllNeededChunkHashes.size());
+ std::vector<bool> DownloadedViaLegacyChunkFlag(AllNeededChunkHashes.size(), false);
+ ChunkBlockAnalyser::BlockResult PartialBlocksResult;
+
+ RemoteProjectStore::GetBlockDescriptionsResult BlockDescriptions = RemoteStore.GetBlockDescriptions(BlockHashes);
+ std::vector<IoHash> BlocksWithDescription;
+ BlocksWithDescription.reserve(BlockDescriptions.Blocks.size());
+ for (const ChunkBlockDescription& BlockDescription : BlockDescriptions.Blocks)
+ {
+ BlocksWithDescription.push_back(BlockDescription.BlockHash);
+ }
+ {
+ auto WantIt = NeededBlockDownloads.begin();
+ auto FindIt = BlockDescriptions.Blocks.begin();
+ while (WantIt != NeededBlockDownloads.end())
+ {
+ if (FindIt == BlockDescriptions.Blocks.end())
+ {
+ // Fall back to full download as we can't get enough information about the block
+ DownloadAndSaveBlock(ChunkStore,
+ RemoteStore,
+ IgnoreMissingAttachments,
+ OptionalContext,
+ NetworkWorkerPool,
+ WorkerPool,
+ AttachmentsDownloadLatch,
+ AttachmentsWriteLatch,
+ RemoteResult,
+ Info,
+ LoadAttachmentsTimer,
+ DownloadStartMS,
+ WantIt->ThinBlockDescription.BlockHash,
+ AllNeededPartialChunkHashesLookup,
+ ChunkDownloadedFlags,
+ 3);
+ for (uint32_t BlockChunkIndex : WantIt->NeededChunkIndexes)
+ {
+ const IoHash& ChunkHash = WantIt->ThinBlockDescription.ChunkRawHashes[BlockChunkIndex];
+ auto It = AllNeededPartialChunkHashesLookup.find(ChunkHash);
+ ZEN_ASSERT(It != AllNeededPartialChunkHashesLookup.end());
+ uint32_t ChunkIndex = It->second;
+ DownloadedViaLegacyChunkFlag[ChunkIndex] = true;
+ }
+ WantIt++;
+ }
+ else if (WantIt->ThinBlockDescription.BlockHash == FindIt->BlockHash)
+ {
+ // Found
+ FindIt++;
+ WantIt++;
+ }
+ else
+ {
+ // Not a requested block?
+ ZEN_ASSERT(false);
+ }
+ }
+ }
+ if (!AllNeededChunkHashes.empty())
+ {
+ std::vector<ChunkBlockAnalyser::EPartialBlockDownloadMode> PartialBlockDownloadModes;
+
+ if (PartialBlockRequestMode == EPartialBlockRequestMode::Off)
+ {
+ PartialBlockDownloadModes.resize(BlocksWithDescription.size(), ChunkBlockAnalyser::EPartialBlockDownloadMode::Off);
+ }
+ else
+ {
+ RemoteProjectStore::AttachmentExistsInCacheResult CacheExistsResult =
+ RemoteStore.AttachmentExistsInCache(BlocksWithDescription);
+ if (CacheExistsResult.ErrorCode != 0 || CacheExistsResult.HasBody.size() != BlocksWithDescription.size())
+ {
+ CacheExistsResult.HasBody.resize(BlocksWithDescription.size(), false);
+ }
+
+ PartialBlockDownloadModes.reserve(BlocksWithDescription.size());
+
+ for (bool ExistsInCache : CacheExistsResult.HasBody)
+ {
+ if (PartialBlockRequestMode == EPartialBlockRequestMode::All)
+ {
+ PartialBlockDownloadModes.push_back(ExistsInCache ? ChunkBlockAnalyser::EPartialBlockDownloadMode::MultiRangeHighSpeed
+ : ChunkBlockAnalyser::EPartialBlockDownloadMode::MultiRange);
+ }
+ else if (PartialBlockRequestMode == EPartialBlockRequestMode::ZenCacheOnly)
+ {
+ PartialBlockDownloadModes.push_back(ExistsInCache ? ChunkBlockAnalyser::EPartialBlockDownloadMode::MultiRangeHighSpeed
+ : ChunkBlockAnalyser::EPartialBlockDownloadMode::Off);
+ }
+ else if (PartialBlockRequestMode == EPartialBlockRequestMode::Mixed)
+ {
+ PartialBlockDownloadModes.push_back(ExistsInCache ? ChunkBlockAnalyser::EPartialBlockDownloadMode::MultiRangeHighSpeed
+ : ChunkBlockAnalyser::EPartialBlockDownloadMode::SingleRange);
+ }
+ }
+ }
+
+ ZEN_ASSERT(PartialBlockDownloadModes.size() == BlocksWithDescription.size());
+
+ ChunkBlockAnalyser PartialAnalyser(*LogOutput,
+ BlockDescriptions.Blocks,
+ ChunkBlockAnalyser::Options{.IsQuiet = false,
+ .IsVerbose = false,
+ .HostLatencySec = HostLatencySec,
+ .HostHighSpeedLatencySec = CacheLatencySec});
+
+ std::vector<ChunkBlockAnalyser::NeededBlock> NeededBlocks =
+ PartialAnalyser.GetNeeded(AllNeededPartialChunkHashesLookup,
+ [&](uint32_t ChunkIndex) { return !DownloadedViaLegacyChunkFlag[ChunkIndex]; });
+
+ PartialBlocksResult = PartialAnalyser.CalculatePartialBlockDownloads(NeededBlocks, PartialBlockDownloadModes);
+ for (uint32_t FullBlockIndex : PartialBlocksResult.FullBlockIndexes)
+ {
+ DownloadAndSaveBlock(ChunkStore,
+ RemoteStore,
+ IgnoreMissingAttachments,
+ OptionalContext,
+ NetworkWorkerPool,
+ WorkerPool,
+ AttachmentsDownloadLatch,
+ AttachmentsWriteLatch,
+ RemoteResult,
+ Info,
+ LoadAttachmentsTimer,
+ DownloadStartMS,
+ BlockDescriptions.Blocks[FullBlockIndex].BlockHash,
+ AllNeededPartialChunkHashesLookup,
+ ChunkDownloadedFlags,
+ 3);
+ }
+
+ for (size_t BlockRangeIndex = 0; BlockRangeIndex < PartialBlocksResult.BlockRanges.size();)
+ {
+ size_t RangeCount = 1;
+ size_t RangesLeft = PartialBlocksResult.BlockRanges.size() - BlockRangeIndex;
+ const ChunkBlockAnalyser::BlockRangeDescriptor& CurrentBlockRange = PartialBlocksResult.BlockRanges[BlockRangeIndex];
+ while (RangeCount < RangesLeft &&
+ CurrentBlockRange.BlockIndex == PartialBlocksResult.BlockRanges[BlockRangeIndex + RangeCount].BlockIndex)
+ {
+ RangeCount++;
+ }
+
+ DownloadAndSavePartialBlock(ChunkStore,
+ RemoteStore,
+ IgnoreMissingAttachments,
+ OptionalContext,
+ NetworkWorkerPool,
+ WorkerPool,
+ AttachmentsDownloadLatch,
+ AttachmentsWriteLatch,
+ RemoteResult,
+ Info,
+ LoadAttachmentsTimer,
+ DownloadStartMS,
+ BlockDescriptions.Blocks[CurrentBlockRange.BlockIndex],
+ PartialBlocksResult.BlockRanges,
+ BlockRangeIndex,
+ RangeCount,
+ AllNeededPartialChunkHashesLookup,
+ ChunkDownloadedFlags,
+ 3);
+
+ BlockRangeIndex += RangeCount;
+ }
+ }
+
AttachmentsDownloadLatch.CountDown();
while (!AttachmentsDownloadLatch.Wait(1000))
{
@@ -3478,21 +3965,30 @@ LoadOplog(CidStore& ChunkStore,
}
}
- remotestore_impl::ReportMessage(
- OptionalContext,
- fmt::format("Loaded oplog '{}' {} in {} ({}), Blocks: {} ({}), Attachments: {} ({}), Stored: {} ({}), Missing: {} {}",
- RemoteStoreInfo.ContainerName,
- Result.ErrorCode == 0 ? "SUCCESS" : "FAILURE",
- NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000.0)),
- NiceBytes(Info.OplogSizeBytes),
- Info.AttachmentBlocksDownloaded.load(),
- NiceBytes(Info.AttachmentBlockBytesDownloaded.load()),
- Info.AttachmentsDownloaded.load(),
- NiceBytes(Info.AttachmentBytesDownloaded.load()),
- Info.AttachmentsStored.load(),
- NiceBytes(Info.AttachmentBytesStored.load()),
- Info.MissingAttachmentCount.load(),
- remotestore_impl::GetStats(RemoteStore.GetStats(), TransferWallTimeMS)));
+ uint64_t TotalDownloads =
+ 1 + Info.AttachmentBlocksDownloaded.load() + Info.AttachmentBlocksRangesDownloaded.load() + Info.AttachmentsDownloaded.load();
+ uint64_t TotalBytesDownloaded = Info.OplogSizeBytes + Info.AttachmentBlockBytesDownloaded.load() +
+ Info.AttachmentBlockRangeBytesDownloaded.load() + Info.AttachmentBytesDownloaded.load();
+
+ remotestore_impl::ReportMessage(OptionalContext,
+ fmt::format("Loaded oplog '{}' {} in {} ({}), Blocks: {} ({}), BlockRanges: {} ({}), Attachments: {} "
+ "({}), Total: {} ({}), Stored: {} ({}), Missing: {} {}",
+ RemoteStoreInfo.ContainerName,
+ Result.ErrorCode == 0 ? "SUCCESS" : "FAILURE",
+ NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000.0)),
+ NiceBytes(Info.OplogSizeBytes),
+ Info.AttachmentBlocksDownloaded.load(),
+ NiceBytes(Info.AttachmentBlockBytesDownloaded.load()),
+ Info.AttachmentBlocksRangesDownloaded.load(),
+ NiceBytes(Info.AttachmentBlockRangeBytesDownloaded.load()),
+ Info.AttachmentsDownloaded.load(),
+ NiceBytes(Info.AttachmentBytesDownloaded.load()),
+ TotalDownloads,
+ NiceBytes(TotalBytesDownloaded),
+ Info.AttachmentsStored.load(),
+ NiceBytes(Info.AttachmentBytesStored.load()),
+ Info.MissingAttachmentCount.load(),
+ remotestore_impl::GetStats(RemoteStore.GetStats(), TransferWallTimeMS)));
return Result;
}
@@ -3697,6 +4193,9 @@ TEST_CASE_TEMPLATE("project.store.export",
/*Force*/ false,
/*IgnoreMissingAttachments*/ false,
/*CleanOplog*/ false,
+ EPartialBlockRequestMode::Mixed,
+ /*HostLatencySec*/ -1.0,
+ /*CacheLatencySec*/ -1.0,
nullptr);
CHECK(ImportResult.ErrorCode == 0);
@@ -3708,6 +4207,9 @@ TEST_CASE_TEMPLATE("project.store.export",
/*Force*/ true,
/*IgnoreMissingAttachments*/ false,
/*CleanOplog*/ false,
+ EPartialBlockRequestMode::Mixed,
+ /*HostLatencySec*/ -1.0,
+ /*CacheLatencySec*/ -1.0,
nullptr);
CHECK(ImportForceResult.ErrorCode == 0);
@@ -3719,6 +4221,9 @@ TEST_CASE_TEMPLATE("project.store.export",
/*Force*/ false,
/*IgnoreMissingAttachments*/ false,
/*CleanOplog*/ true,
+ EPartialBlockRequestMode::Mixed,
+ /*HostLatencySec*/ -1.0,
+ /*CacheLatencySec*/ -1.0,
nullptr);
CHECK(ImportCleanResult.ErrorCode == 0);
@@ -3730,6 +4235,9 @@ TEST_CASE_TEMPLATE("project.store.export",
/*Force*/ true,
/*IgnoreMissingAttachments*/ false,
/*CleanOplog*/ true,
+ EPartialBlockRequestMode::Mixed,
+ /*HostLatencySec*/ -1.0,
+ /*CacheLatencySec*/ -1.0,
nullptr);
CHECK(ImportForceCleanResult.ErrorCode == 0);
}
diff --git a/src/zenremotestore/projectstore/zenremoteprojectstore.cpp b/src/zenremotestore/projectstore/zenremoteprojectstore.cpp
index ab82edbef..b4c1156ac 100644
--- a/src/zenremotestore/projectstore/zenremoteprojectstore.cpp
+++ b/src/zenremotestore/projectstore/zenremoteprojectstore.cpp
@@ -249,7 +249,18 @@ public:
return GetKnownBlocksResult{{.ErrorCode = static_cast<int>(HttpResponseCode::NoContent)}};
}
- virtual LoadAttachmentResult LoadAttachment(const IoHash& RawHash) override
+ virtual GetBlockDescriptionsResult GetBlockDescriptions(std::span<const IoHash> BlockHashes) override
+ {
+ ZEN_UNUSED(BlockHashes);
+ return GetBlockDescriptionsResult{Result{.ErrorCode = int(HttpResponseCode::NotFound)}};
+ }
+
+ virtual AttachmentExistsInCacheResult AttachmentExistsInCache(std::span<const IoHash> RawHashes) override
+ {
+ return AttachmentExistsInCacheResult{Result{.ErrorCode = 0}, std::vector<bool>(RawHashes.size(), false)};
+ }
+
+ virtual LoadAttachmentResult LoadAttachment(const IoHash& RawHash, const AttachmentRange& Range) override
{
std::string LoadRequest = fmt::format("/{}/oplog/{}/{}"sv, m_Project, m_Oplog, RawHash);
HttpClient::Response Response =
@@ -257,12 +268,7 @@ public:
AddStats(Response);
LoadAttachmentResult Result = LoadAttachmentResult{ConvertResult(Response)};
- if (!Result.ErrorCode)
- {
- Result.Bytes = Response.ResponsePayload;
- Result.Bytes.MakeOwned();
- }
- if (!Result.ErrorCode)
+ if (Result.ErrorCode)
{
Result.Reason = fmt::format("Failed fetching oplog attachment from {}/{}/{}/{}. Reason: '{}'",
m_ProjectStoreUrl,
@@ -271,6 +277,15 @@ public:
RawHash,
Result.Reason);
}
+ if (!Result.ErrorCode && Range)
+ {
+ Result.Bytes = IoBuffer(Response.ResponsePayload, Range.Offset, Range.Bytes);
+ }
+ else
+ {
+ Result.Bytes = Response.ResponsePayload;
+ }
+ Result.Bytes.MakeOwned();
return Result;
}
diff --git a/src/zenserver/compute/computeserver.cpp b/src/zenserver/compute/computeserver.cpp
index 173f56386..0f9ef0287 100644
--- a/src/zenserver/compute/computeserver.cpp
+++ b/src/zenserver/compute/computeserver.cpp
@@ -82,7 +82,7 @@ ZenComputeServer::Initialize(const ZenComputeServerConfig& ServerConfig, ZenServ
ZEN_TRACE_CPU("ZenComputeServer::Initialize");
ZEN_MEMSCOPE(GetZenserverTag());
- ZEN_INFO(ZEN_APP_NAME " initializing in HUB server mode");
+ ZEN_INFO(ZEN_APP_NAME " initializing in COMPUTE server mode");
const int EffectiveBasePort = ZenServerBase::Initialize(ServerConfig, ServerEntry);
if (EffectiveBasePort < 0)
@@ -91,7 +91,7 @@ ZenComputeServer::Initialize(const ZenComputeServerConfig& ServerConfig, ZenServ
}
// This is a workaround to make sure we can have automated tests. Without
- // this the ranges for different child zen hub processes could overlap with
+ // this the ranges for different child zen compute processes could overlap with
// the main test range.
ZenServerEnvironment::SetBaseChildId(1000);
@@ -109,7 +109,7 @@ ZenComputeServer::Initialize(const ZenComputeServerConfig& ServerConfig, ZenServ
void
ZenComputeServer::Cleanup()
{
- ZEN_TRACE_CPU("ZenStorageServer::Cleanup");
+ ZEN_TRACE_CPU("ZenComputeServer::Cleanup");
ZEN_INFO(ZEN_APP_NAME " cleaning up");
try
{
diff --git a/src/zenserver/frontend/frontend.cpp b/src/zenserver/frontend/frontend.cpp
index 2b157581f..579a65c5a 100644
--- a/src/zenserver/frontend/frontend.cpp
+++ b/src/zenserver/frontend/frontend.cpp
@@ -38,7 +38,7 @@ HttpFrontendService::HttpFrontendService(std::filesystem::path Directory, HttpSt
#if ZEN_EMBED_HTML_ZIP
// Load an embedded Zip archive
IoBuffer HtmlZipDataBuffer(IoBuffer::Wrap, gHtmlZipData, sizeof(gHtmlZipData) - 1);
- m_ZipFs = ZipFs(std::move(HtmlZipDataBuffer));
+ m_ZipFs = std::make_unique<ZipFs>(std::move(HtmlZipDataBuffer));
#endif
if (m_Directory.empty() && !m_ZipFs)
@@ -114,6 +114,8 @@ HttpFrontendService::HandleRequest(zen::HttpServerRequest& Request)
{
using namespace std::literals;
+ ExtendableStringBuilder<256> UriBuilder;
+
std::string_view Uri = Request.RelativeUriWithExtension();
for (; Uri.length() > 0 && Uri[0] == '/'; Uri = Uri.substr(1))
;
@@ -121,6 +123,11 @@ HttpFrontendService::HandleRequest(zen::HttpServerRequest& Request)
{
Uri = "index.html"sv;
}
+ else if (Uri.back() == '/')
+ {
+ UriBuilder << Uri << "index.html"sv;
+ Uri = UriBuilder;
+ }
// Dismiss if the URI contains .. anywhere to prevent arbitrary file reads
if (Uri.find("..") != Uri.npos)
@@ -145,24 +152,47 @@ HttpFrontendService::HandleRequest(zen::HttpServerRequest& Request)
return Request.WriteResponse(HttpResponseCode::Forbidden);
}
- // The given content directory overrides any zip-fs discovered in the binary
- if (!m_Directory.empty())
- {
- auto FullPath = m_Directory / std::filesystem::path(Uri).make_preferred();
- FileContents File = ReadFile(FullPath);
+ auto WriteResponseForUri = [this,
+ &Request](std::string_view InUri, HttpResponseCode ResponseCode, HttpContentType ContentType) -> bool {
+ // The given content directory overrides any zip-fs discovered in the binary
+ if (!m_Directory.empty())
+ {
+ auto FullPath = m_Directory / std::filesystem::path(InUri).make_preferred();
+ FileContents File = ReadFile(FullPath);
- if (!File.ErrorCode)
+ if (!File.ErrorCode)
+ {
+ Request.WriteResponse(ResponseCode, ContentType, File.Data[0]);
+
+ return true;
+ }
+ }
+
+ if (m_ZipFs)
{
- return Request.WriteResponse(HttpResponseCode::OK, ContentType, File.Data[0]);
+ if (IoBuffer FileBuffer = m_ZipFs->GetFile(InUri))
+ {
+ Request.WriteResponse(HttpResponseCode::OK, ContentType, FileBuffer);
+
+ return true;
+ }
}
- }
- if (IoBuffer FileBuffer = m_ZipFs.GetFile(Uri))
+ return false;
+ };
+
+ if (WriteResponseForUri(Uri, HttpResponseCode::OK, ContentType))
{
- return Request.WriteResponse(HttpResponseCode::OK, ContentType, FileBuffer);
+ return;
+ }
+ else if (WriteResponseForUri("404.html"sv, HttpResponseCode::NotFound, HttpContentType::kHTML))
+ {
+ return;
+ }
+ else
+ {
+ Request.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, "Not found"sv);
}
-
- Request.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, "Not found"sv);
}
} // namespace zen
diff --git a/src/zenserver/frontend/frontend.h b/src/zenserver/frontend/frontend.h
index 84ffaac42..6d8585b72 100644
--- a/src/zenserver/frontend/frontend.h
+++ b/src/zenserver/frontend/frontend.h
@@ -7,6 +7,7 @@
#include "zipfs.h"
#include <filesystem>
+#include <memory>
namespace zen {
@@ -20,9 +21,9 @@ public:
virtual void HandleStatusRequest(HttpServerRequest& Request) override;
private:
- ZipFs m_ZipFs;
- std::filesystem::path m_Directory;
- HttpStatusService& m_StatusService;
+ std::unique_ptr<ZipFs> m_ZipFs;
+ std::filesystem::path m_Directory;
+ HttpStatusService& m_StatusService;
};
} // namespace zen
diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip
index 4767029c0..3d90c18a8 100644
--- a/src/zenserver/frontend/html.zip
+++ b/src/zenserver/frontend/html.zip
Binary files differ
diff --git a/src/zenserver/frontend/zipfs.cpp b/src/zenserver/frontend/zipfs.cpp
index f9c2bc8ff..42df0520f 100644
--- a/src/zenserver/frontend/zipfs.cpp
+++ b/src/zenserver/frontend/zipfs.cpp
@@ -149,13 +149,25 @@ ZipFs::ZipFs(IoBuffer&& Buffer)
IoBuffer
ZipFs::GetFile(const std::string_view& FileName) const
{
- FileMap::iterator Iter = m_Files.find(FileName);
- if (Iter == m_Files.end())
{
- return {};
+ RwLock::SharedLockScope _(m_FilesLock);
+
+ FileMap::const_iterator Iter = m_Files.find(FileName);
+ if (Iter == m_Files.end())
+ {
+ return {};
+ }
+
+ const FileItem& Item = Iter->second;
+ if (Item.GetSize() > 0)
+ {
+ return IoBuffer(IoBuffer::Wrap, Item.GetData(), Item.GetSize());
+ }
}
- FileItem& Item = Iter->second;
+ RwLock::ExclusiveLockScope _(m_FilesLock);
+
+ FileItem& Item = m_Files.find(FileName)->second;
if (Item.GetSize() > 0)
{
return IoBuffer(IoBuffer::Wrap, Item.GetData(), Item.GetSize());
diff --git a/src/zenserver/frontend/zipfs.h b/src/zenserver/frontend/zipfs.h
index 1fa7da451..645121693 100644
--- a/src/zenserver/frontend/zipfs.h
+++ b/src/zenserver/frontend/zipfs.h
@@ -3,23 +3,23 @@
#pragma once
#include <zencore/iobuffer.h>
+#include <zencore/thread.h>
#include <unordered_map>
namespace zen {
-//////////////////////////////////////////////////////////////////////////
class ZipFs
{
public:
- ZipFs() = default;
- ZipFs(IoBuffer&& Buffer);
+ explicit ZipFs(IoBuffer&& Buffer);
+
IoBuffer GetFile(const std::string_view& FileName) const;
- inline operator bool() const { return !m_Files.empty(); }
private:
using FileItem = MemoryView;
using FileMap = std::unordered_map<std::string_view, FileItem>;
+ mutable RwLock m_FilesLock;
FileMap mutable m_Files;
IoBuffer m_Buffer;
};
diff --git a/src/zenserver/hub/hubservice.cpp b/src/zenserver/hub/hubservice.cpp
index 4d9da3a57..a00446a75 100644
--- a/src/zenserver/hub/hubservice.cpp
+++ b/src/zenserver/hub/hubservice.cpp
@@ -151,6 +151,7 @@ struct StorageServerInstance
inline uint16_t GetBasePort() const { return m_ServerInstance.GetBasePort(); }
private:
+ void WakeLocked();
RwLock m_Lock;
std::string m_ModuleId;
std::atomic<bool> m_IsProvisioned{false};
@@ -211,7 +212,7 @@ StorageServerInstance::Provision()
if (m_IsHibernated)
{
- Wake();
+ WakeLocked();
}
else
{
@@ -294,9 +295,14 @@ StorageServerInstance::Hibernate()
void
StorageServerInstance::Wake()
{
- // Start server in-place using existing data
-
RwLock::ExclusiveLockScope _(m_Lock);
+ WakeLocked();
+}
+
+void
+StorageServerInstance::WakeLocked()
+{
+ // Start server in-place using existing data
if (!m_IsHibernated)
{
diff --git a/src/zenserver/hub/zenhubserver.cpp b/src/zenserver/hub/zenhubserver.cpp
index 7a4ba951d..d0a0db417 100644
--- a/src/zenserver/hub/zenhubserver.cpp
+++ b/src/zenserver/hub/zenhubserver.cpp
@@ -105,7 +105,7 @@ ZenHubServer::Initialize(const ZenHubServerConfig& ServerConfig, ZenServerState:
void
ZenHubServer::Cleanup()
{
- ZEN_TRACE_CPU("ZenStorageServer::Cleanup");
+ ZEN_TRACE_CPU("ZenHubServer::Cleanup");
ZEN_INFO(ZEN_APP_NAME " cleaning up");
try
{
diff --git a/src/zenserver/main.cpp b/src/zenserver/main.cpp
index ee783d2a6..571dd3b4f 100644
--- a/src/zenserver/main.cpp
+++ b/src/zenserver/main.cpp
@@ -267,6 +267,14 @@ main(int argc, char* argv[])
using namespace zen;
using namespace std::literals;
+ // note: doctest has locally (in thirdparty) been fixed to not cause shutdown
+ // crashes due to TLS destructors
+ //
+ // mimalloc on the other hand might still be causing issues, in which case
+ // we should work out either how to eliminate the mimalloc dependency or how
+ // to configure it in a way that doesn't cause shutdown issues
+
+#if 0
auto _ = zen::MakeGuard([] {
// Allow some time for worker threads to unravel, in an effort
// to prevent shutdown races in TLS object destruction, mainly due to
@@ -277,6 +285,7 @@ main(int argc, char* argv[])
// shutdown crashes observed in some situations.
WaitForThreads(1000);
});
+#endif
enum
{
diff --git a/src/zenserver/storage/buildstore/httpbuildstore.cpp b/src/zenserver/storage/buildstore/httpbuildstore.cpp
index f5ba30616..bf7afcc02 100644
--- a/src/zenserver/storage/buildstore/httpbuildstore.cpp
+++ b/src/zenserver/storage/buildstore/httpbuildstore.cpp
@@ -185,7 +185,7 @@ HttpBuildStoreService::GetBlobRequest(HttpRouterRequest& Req)
{
const HttpRange& Range = Ranges.front();
const uint64_t BlobSize = Blob.GetSize();
- const uint64_t MaxBlobSize = Range.Start < BlobSize ? Range.Start - BlobSize : 0;
+ const uint64_t MaxBlobSize = Range.Start < BlobSize ? BlobSize - Range.Start : 0;
const uint64_t RangeSize = Min(Range.End - Range.Start + 1, MaxBlobSize);
if (Range.Start + RangeSize > BlobSize)
{
diff --git a/src/zenserver/storage/projectstore/httpprojectstore.cpp b/src/zenserver/storage/projectstore/httpprojectstore.cpp
index 416e2ed69..2b5474d00 100644
--- a/src/zenserver/storage/projectstore/httpprojectstore.cpp
+++ b/src/zenserver/storage/projectstore/httpprojectstore.cpp
@@ -244,6 +244,8 @@ namespace {
{
std::shared_ptr<RemoteProjectStore> Store;
std::string Description;
+ double HostLatencySec = -1.0;
+ double CacheLatencySec = -1.0;
};
CreateRemoteStoreResult CreateRemoteStore(LoggerRef InLog,
@@ -261,6 +263,8 @@ namespace {
using namespace std::literals;
std::shared_ptr<RemoteProjectStore> RemoteStore;
+ double HostLatencySec = -1.0;
+ double CacheLatencySec = -1.0;
if (CbObjectView File = Params["file"sv].AsObjectView(); File)
{
@@ -495,7 +499,9 @@ namespace {
/*Quiet*/ false,
/*Unattended*/ false,
/*Hidden*/ true,
- GetTinyWorkerPool(EWorkloadType::Background));
+ GetTinyWorkerPool(EWorkloadType::Background),
+ HostLatencySec,
+ CacheLatencySec);
}
if (!RemoteStore)
@@ -503,7 +509,10 @@ namespace {
return {nullptr, "Unknown remote store type"};
}
- return {std::move(RemoteStore), ""};
+ return CreateRemoteStoreResult{.Store = std::move(RemoteStore),
+ .Description = "",
+ .HostLatencySec = HostLatencySec,
+ .CacheLatencySec = CacheLatencySec};
}
std::pair<HttpResponseCode, std::string> ConvertResult(const RemoteProjectStore::Result& Result)
@@ -2356,15 +2365,19 @@ HttpProjectService::HandleOplogSaveRequest(HttpRouterRequest& Req)
tsl::robin_set<IoHash, IoHash::Hasher> Attachments;
auto HasAttachment = [this](const IoHash& RawHash) { return m_CidStore.ContainsChunk(RawHash); };
- auto OnNeedBlock = [&AttachmentsLock, &Attachments](const IoHash& BlockHash, const std::vector<IoHash>&& ChunkHashes) {
+ auto OnNeedBlock = [&AttachmentsLock, &Attachments](ThinChunkBlockDescription&& ThinBlockDescription,
+ std::vector<uint32_t>&& NeededChunkIndexes) {
RwLock::ExclusiveLockScope _(AttachmentsLock);
- if (BlockHash != IoHash::Zero)
+ if (ThinBlockDescription.BlockHash != IoHash::Zero)
{
- Attachments.insert(BlockHash);
+ Attachments.insert(ThinBlockDescription.BlockHash);
}
else
{
- Attachments.insert(ChunkHashes.begin(), ChunkHashes.end());
+ for (uint32_t ChunkIndex : NeededChunkIndexes)
+ {
+ Attachments.insert(ThinBlockDescription.ChunkRawHashes[ChunkIndex]);
+ }
}
};
auto OnNeedAttachment = [&AttachmentsLock, &Attachments](const IoHash& RawHash) {
@@ -2663,6 +2676,8 @@ HttpProjectService::HandleRpcRequest(HttpRouterRequest& Req)
bool CleanOplog = Params["clean"].AsBool(false);
bool BoostWorkerCount = Params["boostworkercount"].AsBool(false);
bool BoostWorkerMemory = Params["boostworkermemory"sv].AsBool(false);
+ EPartialBlockRequestMode PartialBlockRequestMode =
+ PartialBlockRequestModeFromString(Params["partialblockrequestmode"sv].AsString("true"));
CreateRemoteStoreResult RemoteStoreResult = CreateRemoteStore(Log(),
Params,
@@ -2688,6 +2703,9 @@ HttpProjectService::HandleRpcRequest(HttpRouterRequest& Req)
Force,
IgnoreMissingAttachments,
CleanOplog,
+ PartialBlockRequestMode,
+ HostLatencySec = RemoteStoreResult.HostLatencySec,
+ CacheLatencySec = RemoteStoreResult.CacheLatencySec,
BoostWorkerCount](JobContext& Context) {
Context.ReportMessage(fmt::format("Loading oplog '{}/{}' from {}",
Oplog->GetOuterProjectIdentifier(),
@@ -2709,6 +2727,9 @@ HttpProjectService::HandleRpcRequest(HttpRouterRequest& Req)
Force,
IgnoreMissingAttachments,
CleanOplog,
+ PartialBlockRequestMode,
+ HostLatencySec,
+ CacheLatencySec,
&Context);
auto Response = ConvertResult(Result);
ZEN_INFO("LoadOplog: Status: {} '{}'", ToString(Response.first), Response.second);
diff --git a/src/zenserver/storage/zenstorageserver.cpp b/src/zenserver/storage/zenstorageserver.cpp
index ff854b72d..3d81db656 100644
--- a/src/zenserver/storage/zenstorageserver.cpp
+++ b/src/zenserver/storage/zenstorageserver.cpp
@@ -700,6 +700,15 @@ ZenStorageServer::Run()
ZEN_INFO(ZEN_APP_NAME " now running (pid: {})", GetCurrentProcessId());
+ if (m_FrontendService)
+ {
+ ZEN_INFO("frontend link: {}", m_Http->GetServiceUri(m_FrontendService.get()));
+ }
+ else
+ {
+ ZEN_INFO("frontend service disabled");
+ }
+
#if ZEN_PLATFORM_WINDOWS
if (zen::windows::IsRunningOnWine())
{
diff --git a/src/zenserver/zenserver.cpp b/src/zenserver/zenserver.cpp
index 7bf6126df..5fd35d9b4 100644
--- a/src/zenserver/zenserver.cpp
+++ b/src/zenserver/zenserver.cpp
@@ -617,6 +617,8 @@ ZenServerMain::Run()
{
ZEN_INFO(ZEN_APP_NAME " unable to grab lock at '{}' (reason: '{}'), retrying", LockFilePath, Ec.message());
Sleep(500);
+
+ m_LockFile.Create(LockFilePath, MakeLockData(false), Ec);
if (Ec)
{
ZEN_WARN(ZEN_APP_NAME " exiting, unable to grab lock at '{}' (reason: '{}')", LockFilePath, Ec.message());
diff --git a/src/zenstore/buildstore/buildstore.cpp b/src/zenstore/buildstore/buildstore.cpp
index 04a0781d3..aa37e75fe 100644
--- a/src/zenstore/buildstore/buildstore.cpp
+++ b/src/zenstore/buildstore/buildstore.cpp
@@ -266,13 +266,12 @@ BuildStore::PutBlob(const IoHash& BlobHash, const IoBuffer& Payload)
m_BlobLookup.insert({BlobHash, NewBlobIndex});
}
- m_LastAccessTimeUpdateCount++;
if (m_TrackedBlobKeys)
{
m_TrackedBlobKeys->push_back(BlobHash);
if (MetadataHash != IoHash::Zero)
{
- m_TrackedBlobKeys->push_back(BlobHash);
+ m_TrackedBlobKeys->push_back(MetadataHash);
}
}
}
diff --git a/src/zenstore/cache/cachedisklayer.cpp b/src/zenstore/cache/cachedisklayer.cpp
index ead7e4f3a..b73b3e6fb 100644
--- a/src/zenstore/cache/cachedisklayer.cpp
+++ b/src/zenstore/cache/cachedisklayer.cpp
@@ -626,7 +626,7 @@ BucketManifestSerializer::ReadSidecarFile(RwLock::ExclusiveLockScope& B
return false;
}
- const uint64_t ExpectedEntryCount = (FileSize - sizeof(sizeof(BucketMetaHeader))) / sizeof(ManifestData);
+ const uint64_t ExpectedEntryCount = (FileSize - sizeof(BucketMetaHeader)) / sizeof(ManifestData);
if (Header.EntryCount > ExpectedEntryCount)
{
ZEN_WARN(
@@ -1057,7 +1057,7 @@ ZenCacheDiskLayer::CacheBucket::ReadIndexFile(RwLock::ExclusiveLockScope&, const
return 0;
}
- const uint64_t ExpectedEntryCount = (FileSize - sizeof(sizeof(cache::impl::CacheBucketIndexHeader))) / sizeof(DiskIndexEntry);
+ const uint64_t ExpectedEntryCount = (FileSize - sizeof(cache::impl::CacheBucketIndexHeader)) / sizeof(DiskIndexEntry);
if (Header.EntryCount > ExpectedEntryCount)
{
return 0;
diff --git a/src/zenstore/cache/cacherpc.cpp b/src/zenstore/cache/cacherpc.cpp
index 94abcf547..e1fd0a3e6 100644
--- a/src/zenstore/cache/cacherpc.cpp
+++ b/src/zenstore/cache/cacherpc.cpp
@@ -966,7 +966,7 @@ CacheRpcHandler::HandleRpcGetCacheRecords(const CacheRequestContext& Context, Cb
}
else
{
- ResponseObject.AddBool(true);
+ ResponseObject.AddBool(false);
}
}
ResponseObject.EndArray();
diff --git a/src/zenstore/cache/structuredcachestore.cpp b/src/zenstore/cache/structuredcachestore.cpp
index 52b494e45..4e8475293 100644
--- a/src/zenstore/cache/structuredcachestore.cpp
+++ b/src/zenstore/cache/structuredcachestore.cpp
@@ -608,7 +608,10 @@ ZenCacheStore::GetBatch::Commit()
m_CacheStore.m_HitCount++;
OpScope.SetBytes(Result.Value.GetSize());
}
- m_CacheStore.m_MissCount++;
+ else
+ {
+ m_CacheStore.m_MissCount++;
+ }
}
}
}
diff --git a/src/zenstore/cas.cpp b/src/zenstore/cas.cpp
index ed017988f..7402d92d3 100644
--- a/src/zenstore/cas.cpp
+++ b/src/zenstore/cas.cpp
@@ -300,12 +300,12 @@ GetCompactCasResults(CasContainerStrategy& Strategy,
};
static void
-GetFileCasResults(FileCasStrategy& Strategy,
- CasStore::InsertMode Mode,
- std::span<IoBuffer> Data,
- std::span<IoHash> ChunkHashes,
- std::span<size_t> Indexes,
- std::vector<CasStore::InsertResult> Results)
+GetFileCasResults(FileCasStrategy& Strategy,
+ CasStore::InsertMode Mode,
+ std::span<IoBuffer> Data,
+ std::span<IoHash> ChunkHashes,
+ std::span<size_t> Indexes,
+ std::vector<CasStore::InsertResult>& Results)
{
for (size_t Index : Indexes)
{
diff --git a/src/zenstore/gc.cpp b/src/zenstore/gc.cpp
index 14caa5abf..c3bdc59f0 100644
--- a/src/zenstore/gc.cpp
+++ b/src/zenstore/gc.cpp
@@ -1494,7 +1494,8 @@ GcManager::CollectGarbage(const GcSettings& Settings)
GcReferenceValidatorStats& Stats = Result.ReferenceValidatorStats[It.second].second;
try
{
- // Go through all the ReferenceCheckers to see if the list of Cids the collector selected are referenced or
+ // Go through all the ReferenceCheckers to see if the list of Cids the collector selected
+ // are referenced or not
SCOPED_TIMER(Stats.ElapsedMS = std::chrono::milliseconds(Timer.GetElapsedTimeMs()););
ReferenceValidator->Validate(Ctx, Stats);
}
@@ -1952,7 +1953,7 @@ GcScheduler::AppendGCLog(std::string_view Id, GcClock::TimePoint StartTime, cons
Writer << "SingleThread"sv << Settings.SingleThread;
Writer << "CompactBlockUsageThresholdPercent"sv << Settings.CompactBlockUsageThresholdPercent;
Writer << "AttachmentRangeMin"sv << Settings.AttachmentRangeMin;
- Writer << "AttachmentRangeMax"sv << Settings.AttachmentRangeMin;
+ Writer << "AttachmentRangeMax"sv << Settings.AttachmentRangeMax;
Writer << "ForceStoreCacheAttachmentMetaData"sv << Settings.StoreCacheAttachmentMetaData;
Writer << "ForceStoreProjectAttachmentMetaData"sv << Settings.StoreProjectAttachmentMetaData;
Writer << "EnableValidation"sv << Settings.EnableValidation;
@@ -2893,7 +2894,7 @@ GcScheduler::CollectGarbage(const GcClock::TimePoint& CacheExpireTime,
{
m_LastFullGCV2Result = Result;
m_LastFullAttachmentRangeMin = AttachmentRangeMin;
- m_LastFullAttachmentRangeMin = AttachmentRangeMax;
+ m_LastFullAttachmentRangeMax = AttachmentRangeMax;
}
Diff.DiskSize = Result.CompactStoresStatSum.RemovedDisk;
Diff.MemorySize = Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory;
diff --git a/src/zenstore/include/zenstore/gc.h b/src/zenstore/include/zenstore/gc.h
index 734d2e5a7..794f50d96 100644
--- a/src/zenstore/include/zenstore/gc.h
+++ b/src/zenstore/include/zenstore/gc.h
@@ -238,7 +238,7 @@ bool FilterReferences(GcCtx& Ctx, std::string_view Context, std::vector<IoHa
/**
* @brief An interface to implement a lock for Stop The World (from writing new data)
*
- * This interface is registered/unregistered to GcManager vua AddGcReferenceLocker() and RemoveGcReferenceLockerr()
+ * This interface is registered/unregistered to GcManager via AddGcReferenceLocker() and RemoveGcReferenceLocker()
*/
class GcReferenceLocker
{
diff --git a/src/zentest-appstub/xmake.lua b/src/zentest-appstub/xmake.lua
index db3ff2e2d..844ba82ef 100644
--- a/src/zentest-appstub/xmake.lua
+++ b/src/zentest-appstub/xmake.lua
@@ -6,8 +6,6 @@ target("zentest-appstub")
add_headerfiles("**.h")
add_files("*.cpp")
add_deps("zencore")
- add_packages("vcpkg::gsl-lite") -- this should ideally be propagated by the zencore dependency
- add_packages("vcpkg::mimalloc")
if is_os("linux") then
add_syslinks("pthread")
diff --git a/src/zenutil/consoletui.cpp b/src/zenutil/consoletui.cpp
new file mode 100644
index 000000000..4410d463d
--- /dev/null
+++ b/src/zenutil/consoletui.cpp
@@ -0,0 +1,483 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zenutil/consoletui.h>
+
+#include <zencore/zencore.h>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+#else
+# include <poll.h>
+# include <sys/ioctl.h>
+# include <termios.h>
+# include <unistd.h>
+#endif
+
+#include <cstdio>
+
+namespace zen {
+
+//////////////////////////////////////////////////////////////////////////
+// Platform-specific terminal helpers
+
+#if ZEN_PLATFORM_WINDOWS
+
+static bool
+CheckIsInteractiveTerminal()
+{
+ DWORD dwMode = 0;
+ return GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &dwMode) && GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &dwMode);
+}
+
+static void
+EnableVirtualTerminal()
+{
+ HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
+ DWORD dwMode = 0;
+ if (GetConsoleMode(hStdOut, &dwMode))
+ {
+ SetConsoleMode(hStdOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
+ }
+}
+
+// RAII guard: sets the console output code page for the lifetime of the object and
+// restores the original on destruction. Required for UTF-8 glyphs to render correctly
+// via printf/fflush since the default console code page is not UTF-8.
+class ConsoleCodePageGuard
+{
+public:
+ explicit ConsoleCodePageGuard(UINT NewCP) : m_OldCP(GetConsoleOutputCP()) { SetConsoleOutputCP(NewCP); }
+ ~ConsoleCodePageGuard() { SetConsoleOutputCP(m_OldCP); }
+
+private:
+ UINT m_OldCP;
+};
+
+enum class ConsoleKey
+{
+ Unknown,
+ ArrowUp,
+ ArrowDown,
+ Enter,
+ Escape,
+};
+
+static ConsoleKey
+ReadKey()
+{
+ HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
+ INPUT_RECORD Record{};
+ DWORD dwRead = 0;
+ while (true)
+ {
+ if (!ReadConsoleInputA(hStdin, &Record, 1, &dwRead))
+ {
+ return ConsoleKey::Escape; // treat read error as cancel
+ }
+ if (Record.EventType == KEY_EVENT && Record.Event.KeyEvent.bKeyDown)
+ {
+ switch (Record.Event.KeyEvent.wVirtualKeyCode)
+ {
+ case VK_UP:
+ return ConsoleKey::ArrowUp;
+ case VK_DOWN:
+ return ConsoleKey::ArrowDown;
+ case VK_RETURN:
+ return ConsoleKey::Enter;
+ case VK_ESCAPE:
+ return ConsoleKey::Escape;
+ default:
+ break;
+ }
+ }
+ }
+}
+
+#else // POSIX
+
+static bool
+CheckIsInteractiveTerminal()
+{
+ return isatty(STDIN_FILENO) && isatty(STDOUT_FILENO);
+}
+
+static void
+EnableVirtualTerminal()
+{
+ // ANSI escape codes are native on POSIX terminals; nothing to do
+}
+
+// RAII guard: switches the terminal to raw/unbuffered input mode and restores
+// the original attributes on destruction.
+class RawModeGuard
+{
+public:
+ RawModeGuard()
+ {
+ if (tcgetattr(STDIN_FILENO, &m_OldAttrs) != 0)
+ {
+ return;
+ }
+
+ struct termios Raw = m_OldAttrs;
+ Raw.c_iflag &= ~static_cast<tcflag_t>(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+ Raw.c_cflag |= CS8;
+ Raw.c_lflag &= ~static_cast<tcflag_t>(ECHO | ICANON | IEXTEN | ISIG);
+ Raw.c_cc[VMIN] = 1;
+ Raw.c_cc[VTIME] = 0;
+ if (tcsetattr(STDIN_FILENO, TCSANOW, &Raw) == 0)
+ {
+ m_Valid = true;
+ }
+ }
+
+ ~RawModeGuard()
+ {
+ if (m_Valid)
+ {
+ tcsetattr(STDIN_FILENO, TCSANOW, &m_OldAttrs);
+ }
+ }
+
+ bool IsValid() const { return m_Valid; }
+
+private:
+ struct termios m_OldAttrs = {};
+ bool m_Valid = false;
+};
+
+static int
+ReadByteWithTimeout(int TimeoutMs)
+{
+ struct pollfd Pfd
+ {
+ STDIN_FILENO, POLLIN, 0
+ };
+ if (poll(&Pfd, 1, TimeoutMs) > 0 && (Pfd.revents & POLLIN))
+ {
+ unsigned char c = 0;
+ if (read(STDIN_FILENO, &c, 1) == 1)
+ {
+ return static_cast<int>(c);
+ }
+ }
+ return -1;
+}
+
+// State for fullscreen live mode (alternate screen + raw input)
+static struct termios s_SavedAttrs = {};
+static bool s_InLiveMode = false;
+
+enum class ConsoleKey
+{
+ Unknown,
+ ArrowUp,
+ ArrowDown,
+ Enter,
+ Escape,
+};
+
+static ConsoleKey
+ReadKey()
+{
+ unsigned char c = 0;
+ if (read(STDIN_FILENO, &c, 1) != 1)
+ {
+ return ConsoleKey::Escape; // treat read error as cancel
+ }
+
+ if (c == 27) // ESC byte or start of an escape sequence
+ {
+ int Next = ReadByteWithTimeout(50);
+ if (Next == '[')
+ {
+ int Final = ReadByteWithTimeout(50);
+ if (Final == 'A')
+ {
+ return ConsoleKey::ArrowUp;
+ }
+ if (Final == 'B')
+ {
+ return ConsoleKey::ArrowDown;
+ }
+ }
+ return ConsoleKey::Escape;
+ }
+
+ if (c == '\r' || c == '\n')
+ {
+ return ConsoleKey::Enter;
+ }
+
+ return ConsoleKey::Unknown;
+}
+
+#endif // ZEN_PLATFORM_WINDOWS / POSIX
+
+//////////////////////////////////////////////////////////////////////////
+// Public API
+
+uint32_t
+TuiConsoleColumns(uint32_t Default)
+{
+#if ZEN_PLATFORM_WINDOWS
+ CONSOLE_SCREEN_BUFFER_INFO Csbi = {};
+ if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &Csbi))
+ {
+ return static_cast<uint32_t>(Csbi.dwSize.X);
+ }
+#else
+ struct winsize Ws = {};
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &Ws) == 0 && Ws.ws_col > 0)
+ {
+ return static_cast<uint32_t>(Ws.ws_col);
+ }
+#endif
+ return Default;
+}
+
+void
+TuiEnableOutput()
+{
+ EnableVirtualTerminal();
+#if ZEN_PLATFORM_WINDOWS
+ SetConsoleOutputCP(CP_UTF8);
+#endif
+}
+
+bool
+TuiIsStdoutTty()
+{
+#if ZEN_PLATFORM_WINDOWS
+ static bool Cached = [] {
+ DWORD dwMode = 0;
+ return GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &dwMode) != 0;
+ }();
+ return Cached;
+#else
+ static bool Cached = isatty(STDOUT_FILENO) != 0;
+ return Cached;
+#endif
+}
+
+bool
+IsTuiAvailable()
+{
+ static bool Cached = CheckIsInteractiveTerminal();
+ return Cached;
+}
+
+int
+TuiPickOne(std::string_view Title, std::span<const std::string> Items)
+{
+ EnableVirtualTerminal();
+
+#if ZEN_PLATFORM_WINDOWS
+ ConsoleCodePageGuard CodePageGuard(CP_UTF8);
+#else
+ RawModeGuard RawMode;
+ if (!RawMode.IsValid())
+ {
+ return -1;
+ }
+#endif
+
+ const int Count = static_cast<int>(Items.size());
+ int SelectedIndex = 0;
+
+ printf("\n%.*s\n\n", static_cast<int>(Title.size()), Title.data());
+
+ // Hide cursor during interaction
+ printf("\033[?25l");
+
+ // Renders the full entry list and hint footer.
+ // On subsequent calls, moves the cursor back up first to overwrite the previous output.
+ bool FirstRender = true;
+ auto RenderAll = [&] {
+ if (!FirstRender)
+ {
+ printf("\033[%dA", Count + 2); // move up: entries + blank line + hint line
+ }
+ FirstRender = false;
+
+ for (int i = 0; i < Count; ++i)
+ {
+ bool IsSelected = (i == SelectedIndex);
+
+ printf("\r\033[K"); // erase line
+
+ if (IsSelected)
+ {
+ printf("\033[1;7m"); // bold + reverse video
+ }
+
+ // \xe2\x96\xb6 = U+25B6 BLACK RIGHT-POINTING TRIANGLE (▶)
+ const char* Indicator = IsSelected ? " \xe2\x96\xb6 " : " ";
+
+ printf("%s%s", Indicator, Items[i].c_str());
+
+ if (IsSelected)
+ {
+ printf("\033[0m"); // reset attributes
+ }
+
+ printf("\n");
+ }
+
+ // Blank separator line
+ printf("\r\033[K\n");
+
+ // Hint footer
+ // \xe2\x86\x91 = U+2191 ↑ \xe2\x86\x93 = U+2193 ↓
+ printf(
+ "\r\033[K \033[2m\xe2\x86\x91/\xe2\x86\x93\033[0m navigate "
+ "\033[2mEnter\033[0m confirm "
+ "\033[2mEsc\033[0m cancel\n");
+
+ fflush(stdout);
+ };
+
+ RenderAll();
+
+ int Result = -1;
+ bool Done = false;
+ while (!Done)
+ {
+ ConsoleKey Key = ReadKey();
+ switch (Key)
+ {
+ case ConsoleKey::ArrowUp:
+ SelectedIndex = (SelectedIndex - 1 + Count) % Count;
+ RenderAll();
+ break;
+
+ case ConsoleKey::ArrowDown:
+ SelectedIndex = (SelectedIndex + 1) % Count;
+ RenderAll();
+ break;
+
+ case ConsoleKey::Enter:
+ Result = SelectedIndex;
+ Done = true;
+ break;
+
+ case ConsoleKey::Escape:
+ Done = true;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ // Restore cursor and add a blank line for visual separation
+ printf("\033[?25h\n");
+ fflush(stdout);
+
+ return Result;
+}
+
+void
+TuiEnterAlternateScreen()
+{
+ EnableVirtualTerminal();
+#if ZEN_PLATFORM_WINDOWS
+ SetConsoleOutputCP(CP_UTF8);
+#endif
+
+ printf("\033[?1049h"); // Enter alternate screen buffer
+ printf("\033[?25l"); // Hide cursor
+ fflush(stdout);
+
+#if !ZEN_PLATFORM_WINDOWS
+ if (tcgetattr(STDIN_FILENO, &s_SavedAttrs) == 0)
+ {
+ struct termios Raw = s_SavedAttrs;
+ Raw.c_iflag &= ~static_cast<tcflag_t>(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+ Raw.c_cflag |= CS8;
+ Raw.c_lflag &= ~static_cast<tcflag_t>(ECHO | ICANON | IEXTEN | ISIG);
+ Raw.c_cc[VMIN] = 1;
+ Raw.c_cc[VTIME] = 0;
+ if (tcsetattr(STDIN_FILENO, TCSANOW, &Raw) == 0)
+ {
+ s_InLiveMode = true;
+ }
+ }
+#endif
+}
+
+void
+TuiExitAlternateScreen()
+{
+ printf("\033[?25h"); // Show cursor
+ printf("\033[?1049l"); // Exit alternate screen buffer
+ fflush(stdout);
+
+#if !ZEN_PLATFORM_WINDOWS
+ if (s_InLiveMode)
+ {
+ tcsetattr(STDIN_FILENO, TCSANOW, &s_SavedAttrs);
+ s_InLiveMode = false;
+ }
+#endif
+}
+
+void
+TuiCursorHome()
+{
+ printf("\033[H");
+}
+
+uint32_t
+TuiConsoleRows(uint32_t Default)
+{
+#if ZEN_PLATFORM_WINDOWS
+ CONSOLE_SCREEN_BUFFER_INFO Csbi = {};
+ if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &Csbi))
+ {
+ return static_cast<uint32_t>(Csbi.srWindow.Bottom - Csbi.srWindow.Top + 1);
+ }
+#else
+ struct winsize Ws = {};
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &Ws) == 0 && Ws.ws_row > 0)
+ {
+ return static_cast<uint32_t>(Ws.ws_row);
+ }
+#endif
+ return Default;
+}
+
+bool
+TuiPollQuit()
+{
+#if ZEN_PLATFORM_WINDOWS
+ HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
+ DWORD dwCount = 0;
+ if (!GetNumberOfConsoleInputEvents(hStdin, &dwCount) || dwCount == 0)
+ {
+ return false;
+ }
+ INPUT_RECORD Record{};
+ DWORD dwRead = 0;
+ while (PeekConsoleInputA(hStdin, &Record, 1, &dwRead) && dwRead > 0)
+ {
+ ReadConsoleInputA(hStdin, &Record, 1, &dwRead);
+ if (Record.EventType == KEY_EVENT && Record.Event.KeyEvent.bKeyDown)
+ {
+ WORD vk = Record.Event.KeyEvent.wVirtualKeyCode;
+ char ch = Record.Event.KeyEvent.uChar.AsciiChar;
+ if (vk == VK_ESCAPE || ch == 'q' || ch == 'Q')
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+#else
+ // Non-blocking read: character 3 = Ctrl+C, 27 = Esc, 'q'/'Q' = quit
+ int b = ReadByteWithTimeout(0);
+ return (b == 3 || b == 27 || b == 'q' || b == 'Q');
+#endif
+}
+
+} // namespace zen
diff --git a/src/zenutil/include/zenutil/consoletui.h b/src/zenutil/include/zenutil/consoletui.h
new file mode 100644
index 000000000..7dc68c126
--- /dev/null
+++ b/src/zenutil/include/zenutil/consoletui.h
@@ -0,0 +1,59 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <span>
+#include <string>
+#include <string_view>
+
+namespace zen {
+
+// Returns the width of the console in columns, or Default if it cannot be determined.
+uint32_t TuiConsoleColumns(uint32_t Default = 120);
+
+// Enables ANSI/VT escape code processing and UTF-8 console output.
+// Call once before printing ANSI escape sequences or multi-byte UTF-8 characters via printf.
+// Safe to call multiple times. No-op on POSIX (escape codes are native there).
+void TuiEnableOutput();
+
+// Returns true if stdout is connected to a real terminal (not piped or redirected).
+// Useful for deciding whether to use ANSI escape codes for progress output.
+bool TuiIsStdoutTty();
+
+// Returns true if both stdin and stdout are connected to an interactive terminal
+// (i.e. not piped or redirected). Must be checked before calling TuiPickOne().
+bool IsTuiAvailable();
+
+// Displays a cursor-navigable single-select list in the terminal.
+//
+// - Title: a short description printed once above the list
+// - Items: pre-formatted display labels, one per selectable entry
+//
+// Arrow keys (↑/↓) navigate the selection, Enter confirms, Esc cancels.
+// Returns the index of the selected item, or -1 if the user cancelled.
+//
+// Precondition: IsTuiAvailable() must be true.
+int TuiPickOne(std::string_view Title, std::span<const std::string> Items);
+
+// Enter the alternate screen buffer for fullscreen live-update mode.
+// Hides the cursor. On POSIX, switches to raw/unbuffered terminal input.
+// Must be balanced by a call to TuiExitAlternateScreen().
+// Precondition: IsTuiAvailable() must be true.
+void TuiEnterAlternateScreen();
+
+// Exit alternate screen buffer. Restores the cursor and, on POSIX, the original
+// terminal mode. Safe to call even if TuiEnterAlternateScreen() was not called.
+void TuiExitAlternateScreen();
+
+// Move the cursor to the top-left corner of the terminal (row 1, col 1).
+void TuiCursorHome();
+
+// Returns the height of the console in rows, or Default if it cannot be determined.
+uint32_t TuiConsoleRows(uint32_t Default = 40);
+
+// Non-blocking check: returns true if the user has pressed a key that means quit
+// (Esc, 'q', 'Q', or Ctrl+C). Consumes the event if one is pending.
+// Should only be called while in alternate screen mode.
+bool TuiPollQuit();
+
+} // namespace zen