activity_sdk_generation_scenario_shards.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. #include <Windows.h>
  2. #include <algorithm>
  3. #include <array>
  4. #include <atomic>
  5. #include <cstddef>
  6. #include <cstdint>
  7. #include <cstdio>
  8. #include <memory>
  9. #include <new>
  10. #include <string>
  11. #include <string_view>
  12. #include <vector>
  13. #include "../../../core/logging/log.h"
  14. #include "../../../state/activity_sdk/generated_world/codec.h"
  15. #include "../../../state/activity_sdk/generation/definition.h"
  16. #include "../../../state/build_data/runtime.h"
  17. #include "activity_sdk_generation_worker_internal.h"
  18. namespace sunrise::client::content::activity::sdk_generation::worker_internal {
  19. namespace {
  20. /** Parallel readers divide one pass-wide cache budget instead of multiplying it per core. */
  21. constexpr std::size_t kParallelBlockCacheBudget = 256;
  22. constexpr std::size_t kParallelTableCacheBudget = 16;
  23. constexpr std::size_t kMaximumScenarioWorkers = 12;
  24. constexpr std::size_t kScenarioChunkSize = 1;
  25. /** Counts the rows behind a refused shard, which the coverage verdict alone cannot name. */
  26. void report_shard_diagnostics(const Scenario& scenario,
  27. const catalog::Snapshot& snapshot) noexcept {
  28. std::size_t incompleteObjects = 0;
  29. std::size_t incompleteSafety = 0;
  30. std::size_t leaves = 0;
  31. std::size_t bareTargets = 0;
  32. for (const catalog::Object& object : snapshot.objects) {
  33. if (!object.complete) {
  34. ++incompleteObjects;
  35. }
  36. if (object.safety == catalog::GroupSafety::incomplete) {
  37. ++incompleteSafety;
  38. }
  39. leaves += object.placedLeafCount;
  40. bareTargets += object.bareTargetCount;
  41. }
  42. std::size_t unresolvedStates = 0;
  43. for (const catalog::State& state : snapshot.states) {
  44. if (!state.resolved) {
  45. ++unresolvedStates;
  46. }
  47. }
  48. const catalog::ContainerPlacementDiagnostics& placements =
  49. snapshot.containerPlacementDiagnostics;
  50. std::array<char, 1024> line{};
  51. const int written =
  52. std::snprintf(line.data(),
  53. line.size(),
  54. "ev=sdk_shard_diag scenario=0x%08X objects=%zu "
  55. "obj_incomplete=%zu obj_safety_incomplete=%zu leaves=%zu "
  56. "bare=%zu states_unresolved=%zu "
  57. "unresolved_reads=%llu spatial_ctx=%u spatial_na=%u spatial_complete=%u "
  58. "spatial_unresolved=%llu spatial_dropped=%llu spatial_semantic=%llu "
  59. "spatial_tables=%zu spatial_owners=%zu spatial_instances=%zu "
  60. "cp_ctx=%u cp_na=%u cp_complete=%u cp_owner_complete=%u "
  61. "cp_unresolved=%llu cp_semantic=%llu cp_dropped=%llu/%llu/%llu/%llu/%llu "
  62. "cp_rows=%zu/%zu/%zu/%zu/%zu",
  63. static_cast<unsigned>(scenario.tag),
  64. snapshot.objects.size(),
  65. incompleteObjects,
  66. incompleteSafety,
  67. leaves,
  68. bareTargets,
  69. unresolvedStates,
  70. static_cast<unsigned long long>(snapshot.unresolvedReads),
  71. snapshot.staticSpatialContextResolved ? 1U : 0U,
  72. snapshot.staticSpatialNotApplicable ? 1U : 0U,
  73. snapshot.staticSpatialComplete ? 1U : 0U,
  74. static_cast<unsigned long long>(snapshot.staticSpatialUnresolvedReads),
  75. static_cast<unsigned long long>(snapshot.staticSpatialDropped),
  76. static_cast<unsigned long long>(snapshot.staticSpatialSemanticUnresolved),
  77. snapshot.staticSpatialTables.size(),
  78. snapshot.staticSpatialOwners.size(),
  79. snapshot.staticSpatialInstances.size(),
  80. placements.contextResolved ? 1U : 0U,
  81. placements.contextNotApplicable ? 1U : 0U,
  82. placements.complete ? 1U : 0U,
  83. placements.identityOwnerInventoryComplete ? 1U : 0U,
  84. static_cast<unsigned long long>(placements.unresolvedReads),
  85. static_cast<unsigned long long>(placements.semanticUnresolved),
  86. static_cast<unsigned long long>(placements.droppedLists),
  87. static_cast<unsigned long long>(placements.droppedOwners),
  88. static_cast<unsigned long long>(placements.droppedPlacements),
  89. static_cast<unsigned long long>(placements.droppedConfigs),
  90. static_cast<unsigned long long>(placements.droppedComponents),
  91. snapshot.containerPlacementLists.size(),
  92. snapshot.containerPlacementOwners.size(),
  93. snapshot.containerPlacements.size(),
  94. snapshot.containerPlacementConfigs.size(),
  95. snapshot.containerPlacementComponents.size());
  96. if (written > 0) {
  97. core::log::write(
  98. core::log::Channel::client,
  99. core::log::Level::error,
  100. {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1)});
  101. }
  102. }
  103. /** Writes one new shard under its deterministic digest name. */
  104. [[nodiscard]] bool build_record(const Work& work,
  105. const package_reader::Source& source,
  106. const builder::ContainerIndex& containers,
  107. package_reader::Scratch& scratch,
  108. builder::ScenarioAnalysisCache& analyses,
  109. const Scenario& scenario,
  110. manifest::Record& record,
  111. std::shared_ptr<const catalog::Snapshot>& output,
  112. std::array<char, kDetailCapacity>& detailScratch,
  113. const char*& detail) {
  114. // Each failure arm reports its own detail token.
  115. const std::string_view name(scenario.name.data(), scenario.nameLength);
  116. const auto snapshot = builder::build_scenario_catalog(
  117. source, containers, scratch, analyses, work.tables, scenario.tag, name, &cancel_requested);
  118. if (cancel_requested()) {
  119. detail = "shard_cancelled";
  120. return false;
  121. }
  122. if (!snapshot) {
  123. detail = "shard_catalog_null";
  124. return false;
  125. }
  126. if (snapshot->status != catalog::BuildStatus::ready) {
  127. const char* stage = "shard_catalog_not_ready";
  128. switch (snapshot->status) {
  129. case catalog::BuildStatus::failed:
  130. stage = "shard_catalog_failed";
  131. break;
  132. case catalog::BuildStatus::idle:
  133. stage = "shard_catalog_idle";
  134. break;
  135. case catalog::BuildStatus::queued:
  136. stage = "shard_catalog_queued";
  137. break;
  138. case catalog::BuildStatus::building:
  139. stage = "shard_catalog_building";
  140. break;
  141. default:
  142. break;
  143. }
  144. report_shard_diagnostics(scenario, *snapshot);
  145. // The builder already names its own refusal. Carry that text out instead of dropping it.
  146. const char* reason = snapshot->detail[0] != '\0' ? snapshot->detail.data() : "no detail";
  147. detailScratch = {};
  148. (void)std::snprintf(detailScratch.data(), detailScratch.size(), "%s:%s", stage, reason);
  149. detail = detailScratch.data();
  150. return false;
  151. }
  152. if (!snapshot->containerPlacementDiagnostics.identityOwnerInventoryComplete) {
  153. detail = "shard_container_inventory_incomplete";
  154. report_shard_diagnostics(scenario, *snapshot);
  155. return false;
  156. }
  157. generated::PreparedShard prepared{};
  158. if (!generated::prepare(work.sourceFingerprint, *snapshot, prepared)) {
  159. detail = "shard_prepare_failed";
  160. return false;
  161. }
  162. const generated::Digest payload = prepared.payload_sha256();
  163. std::wstring finalPath;
  164. generated::Digest written{};
  165. if (!shard_path(work.scenarioDirectory, scenario.tag, payload, finalPath)) {
  166. detail = "shard_path_failed";
  167. return false;
  168. }
  169. if (!generated::publish(finalPath.c_str(), std::move(prepared), written)) {
  170. detail = "shard_write_failed";
  171. return false;
  172. }
  173. record = {};
  174. record.scenarioTag = scenario.tag;
  175. record.scenarioName = scenario.name;
  176. record.scenarioNameLength = scenario.nameLength;
  177. record.shardPayloadSha256 = payload;
  178. output = snapshot;
  179. return true;
  180. }
  181. /** Rewrites one loaded cache hit into an isolated output tree. */
  182. [[nodiscard]] bool
  183. materialize_cached_record(const Work& work,
  184. const Scenario& scenario,
  185. const manifest::Record& record,
  186. const std::shared_ptr<const catalog::Snapshot>& cached,
  187. std::shared_ptr<const catalog::Snapshot>& output) noexcept {
  188. if (cached == nullptr) {
  189. return false;
  190. }
  191. if (work.cacheScenarioDirectory == work.scenarioDirectory) {
  192. output = cached;
  193. return true;
  194. }
  195. std::wstring finalPath;
  196. generated::Digest written{};
  197. if (!shard_path(work.scenarioDirectory, scenario.tag, record.shardPayloadSha256, finalPath)
  198. || !generated::write(finalPath.c_str(), work.sourceFingerprint, *cached, written)) {
  199. return false;
  200. }
  201. output = cached;
  202. return true;
  203. }
  204. /** Shared immutable inputs and atomic scheduling state for one scenario batch. */
  205. struct ScenarioBuildBatch final {
  206. const Work* work{};
  207. const package_reader::Source* source{};
  208. const builder::ContainerIndex* containers{};
  209. const manifest::Catalog* prior{};
  210. std::vector<ScenarioBuildResult>* results{};
  211. std::atomic_size_t next{};
  212. std::atomic_size_t completed{};
  213. SRWLOCK progressLock = SRWLOCK_INIT;
  214. std::size_t progressPublished{};
  215. std::size_t firstScenario{};
  216. std::size_t blockCacheSlots{};
  217. std::size_t tableCacheSlots{};
  218. bool priorReady{};
  219. };
  220. /** Caps workers while leaving cores for the game and the server. */
  221. [[nodiscard]] std::size_t scenario_worker_count(std::size_t scenarios) noexcept {
  222. SYSTEM_INFO info{};
  223. GetSystemInfo(&info);
  224. const std::size_t processors = static_cast<std::size_t>(info.dwNumberOfProcessors);
  225. const std::size_t available = processors > 2U ? processors - 2U : 1U;
  226. return (std::min)(scenarios, (std::min)(available, kMaximumScenarioWorkers));
  227. }
  228. /** Divides one cache budget across active workers without leaving a worker uncached. */
  229. [[nodiscard]] std::size_t worker_cache_slots(std::size_t budget, std::size_t workers) noexcept {
  230. return (std::max)(std::size_t{1}, (budget + workers - 1U) / workers);
  231. }
  232. /** Publishes only increasing completion counts from out-of-order workers. */
  233. void publish_parallel_progress(ScenarioBuildBatch& batch, const Scenario& scenario) noexcept {
  234. const std::size_t complete = batch.completed.fetch_add(1U) + 1U;
  235. AcquireSRWLockExclusive(&batch.progressLock);
  236. if (complete <= batch.progressPublished) {
  237. ReleaseSRWLockExclusive(&batch.progressLock);
  238. return;
  239. }
  240. batch.progressPublished = complete;
  241. publish_progress(state::activity_sdk::generation::Status::building,
  242. static_cast<std::uint32_t>(complete),
  243. static_cast<std::uint32_t>(batch.work->scenarios.size()),
  244. scenario.tag,
  245. std::string_view(scenario.name.data(), scenario.nameLength));
  246. ReleaseSRWLockExclusive(&batch.progressLock);
  247. }
  248. /** A cached shard of a laid-out scenario is reusable only when its placement contexts resolved. */
  249. [[nodiscard]] bool placement_context_ready(const Work& work,
  250. const Scenario& scenario,
  251. const catalog::Snapshot& snapshot) noexcept {
  252. state::build_data::scenarios::Definition layout{};
  253. const std::string_view name(scenario.name.data(), scenario.nameLength);
  254. if (!::sunrise::client::content::scenarios::find_layout(work.tables.layouts, name, layout)
  255. || layout.spawnStemLength == 0) {
  256. return true;
  257. }
  258. return snapshot.containerPlacementDiagnostics.contextResolved
  259. && snapshot.staticSpatialContextResolved;
  260. }
  261. /** Builds one worker's contiguous chunks with private package and analysis caches. */
  262. void run_scenario_worker(ScenarioBuildBatch& batch) noexcept {
  263. std::unique_ptr<package_reader::Scratch> scratch(new (std::nothrow) package_reader::Scratch());
  264. if (scratch == nullptr) {
  265. return;
  266. }
  267. if (!package_reader::prepare_blocks(*scratch, batch.blockCacheSlots)) {
  268. (void)package_reader::prepare_blocks(*scratch, 0);
  269. }
  270. if (!package_reader::prepare_tables(*scratch, batch.tableCacheSlots)) {
  271. (void)package_reader::prepare_tables(*scratch, 0);
  272. }
  273. builder::ScenarioAnalysisCache analyses{};
  274. while (!cancel_requested()) {
  275. const std::size_t begin = batch.next.fetch_add(kScenarioChunkSize);
  276. if (begin >= batch.results->size()) {
  277. break;
  278. }
  279. const std::size_t end = (std::min)(begin + kScenarioChunkSize, batch.results->size());
  280. for (std::size_t index = begin; index < end && !cancel_requested(); ++index) {
  281. const Scenario& scenario = batch.work->scenarios[batch.firstScenario + index];
  282. ScenarioBuildResult& result = (*batch.results)[index];
  283. result.attempted = true;
  284. try {
  285. const manifest::Record* existing =
  286. batch.priorReady ? find_record(*batch.prior, scenario.tag) : nullptr;
  287. bool kept = existing != nullptr
  288. && load_full_record(batch.work->cacheScenarioDirectory,
  289. batch.work->sourceFingerprint,
  290. scenario,
  291. *existing,
  292. result.snapshot);
  293. if (kept && !placement_context_ready(*batch.work, scenario, *result.snapshot)) {
  294. kept = false;
  295. }
  296. if (kept
  297. && !materialize_cached_record(
  298. *batch.work, scenario, *existing, result.snapshot, result.snapshot)) {
  299. kept = false;
  300. }
  301. if (kept) {
  302. result.record = *existing;
  303. result.reused = true;
  304. result.ready = true;
  305. } else {
  306. const char* detail = "shard_build_failed";
  307. std::array<char, kDetailCapacity> scratchDetail{};
  308. result.ready = build_record(*batch.work,
  309. *batch.source,
  310. *batch.containers,
  311. *scratch,
  312. analyses,
  313. scenario,
  314. result.record,
  315. result.snapshot,
  316. scratchDetail,
  317. detail);
  318. if (!result.ready) {
  319. (void)std::snprintf(
  320. result.detail.data(), result.detail.size(), "%s", detail);
  321. }
  322. }
  323. } catch (...) {
  324. result.ready = false;
  325. (void)std::snprintf(result.detail.data(),
  326. result.detail.size(),
  327. "%s",
  328. "unexpected scenario build exception");
  329. }
  330. publish_parallel_progress(batch, scenario);
  331. }
  332. }
  333. package_reader::close_files(*scratch);
  334. }
  335. /** Adapts one batch worker to the Windows thread ABI. */
  336. DWORD WINAPI scenario_thread_main(void* opaque) noexcept {
  337. run_scenario_worker(*static_cast<ScenarioBuildBatch*>(opaque));
  338. return 0;
  339. }
  340. } // namespace
  341. /** Builds all scenario snapshots in parallel while retaining scenario-order output. */
  342. bool build_scenarios(Work& work,
  343. const package_reader::Source& source,
  344. const builder::ContainerIndex& containers,
  345. const manifest::Catalog& prior,
  346. bool priorReady,
  347. std::size_t firstScenario,
  348. std::size_t scenarioCount,
  349. std::vector<ScenarioBuildResult>& results) {
  350. results.clear();
  351. results.resize(scenarioCount);
  352. ScenarioBuildBatch batch{};
  353. batch.work = &work;
  354. batch.source = &source;
  355. batch.containers = &containers;
  356. batch.prior = &prior;
  357. batch.results = &results;
  358. batch.completed.store(firstScenario);
  359. batch.progressPublished = firstScenario;
  360. batch.firstScenario = firstScenario;
  361. batch.priorReady = priorReady;
  362. const std::size_t workers = scenario_worker_count(results.size());
  363. if (workers == 0) {
  364. return false;
  365. }
  366. batch.blockCacheSlots = worker_cache_slots(kParallelBlockCacheBudget, workers);
  367. batch.tableCacheSlots = worker_cache_slots(kParallelTableCacheBudget, workers);
  368. std::vector<HANDLE> threads;
  369. threads.reserve(workers - 1U);
  370. for (std::size_t index = 1; index < workers; ++index) {
  371. const HANDLE thread = CreateThread(nullptr, 0, &scenario_thread_main, &batch, 0, nullptr);
  372. if (thread != nullptr) {
  373. threads.push_back(thread);
  374. }
  375. }
  376. run_scenario_worker(batch);
  377. for (const HANDLE thread : threads) {
  378. (void)WaitForSingleObject(thread, INFINITE);
  379. (void)CloseHandle(thread);
  380. }
  381. return !cancel_requested();
  382. }
  383. } // namespace sunrise::client::content::activity::sdk_generation::worker_internal