feat(ugc): each make's CPU time and memory; durations in readable units

The UGC server records, with each successful make's time, the worker
thread's CPU time for it and the memory it estimated the job needs (the
figure its memory budget counts; not a measurement)
(process_cpu_ms and process_memory_kb on ugc and ugc_modular_build;
migrations sqlite 74, mysql 91). The UGC page shows them with the time
and duration ("took 12.4 s, CPU 11.9 s, ~96 MB RAM (est.)"), and
durations use the largest units that fit, up to days.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 20:33:09 -05:00
parent 432b8e231e
commit cd81cf4c74
20 changed files with 89 additions and 31 deletions

View File

@@ -191,7 +191,7 @@ namespace {
return { { "id", std::to_string(entry.id) }, { "characterId", std::to_string(entry.characterId) }, { "characterName", entry.characterName }, return { { "id", std::to_string(entry.id) }, { "characterId", std::to_string(entry.characterId) }, { "characterName", entry.characterName },
{ "accountId", entry.accountId }, { "accountName", entry.accountName }, { "state", IUgc::ProcessStateName(entry.state) }, { "attempts", entry.attempts }, { "accountId", entry.accountId }, { "accountName", entry.accountName }, { "state", IUgc::ProcessStateName(entry.state) }, { "attempts", entry.attempts },
{ "processedAt", entry.processedAt }, { "error", entry.error }, { "bakeAo", entry.bakeAo }, { "processAfter", entry.processAfter }, { "processedAt", entry.processedAt }, { "error", entry.error }, { "bakeAo", entry.bakeAo }, { "processAfter", entry.processAfter },
{ "detail", entry.detail }, { "bricks", entry.bricks }, { "triangles", entry.triangles }, { "processMs", entry.processMs } }; { "detail", entry.detail }, { "bricks", entry.bricks }, { "triangles", entry.triangles }, { "processMs", entry.processMs }, { "processCpuMs", entry.processCpuMs }, { "processMemoryKb", entry.processMemoryKb } };
} }
// Web thread: every car and rocket build (the assemblies are made from them) // Web thread: every car and rocket build (the assemblies are made from them)

View File

@@ -89,16 +89,26 @@
return '<img src="' + esc(fileUrl(list.kind, iconId(i), 'icon.png')) + '" width="' + size + '" height="' + size + '" loading="lazy" alt="" class="ugc-checker rounded" onerror="this.style.visibility=\'hidden\'">'; return '<img src="' + esc(fileUrl(list.kind, iconId(i), 'icon.png')) + '" width="' + size + '" height="' + size + '" loading="lazy" alt="" class="ugc-checker rounded" onerror="this.style.visibility=\'hidden\'">';
} }
function moduleNames(i) { return (i.moduleList || []).map(function (m) { return m.name || m.lot; }).join(', '); } function moduleNames(i) { return (i.moduleList || []).map(function (m) { return m.name || m.lot; }).join(', '); }
// How long a make took: "850 ms", "12.4 s", "2 min 5 s" // A duration in the largest units that fit: "850 ms", "12.4 s", "2 min 5 s", "1 h 4 min", "2 d 3 h"
function duration(ms) { function duration(ms) {
ms = Math.round(ms);
if (ms < 1000) return ms + ' ms'; if (ms < 1000) return ms + ' ms';
if (ms < 60000) return (ms / 1000).toFixed(1) + ' s'; if (ms < 60000) return (ms / 1000).toFixed(ms < 10000 ? 2 : 1) + ' s';
return Math.floor(ms / 60000) + ' min ' + Math.round((ms % 60000) / 1000) + ' s'; var s = Math.round(ms / 1000), m = Math.floor(s / 60), h = Math.floor(m / 60), d = Math.floor(h / 24);
if (d) return d + ' d' + (h % 24 ? ' ' + (h % 24) + ' h' : '');
if (h) return h + ' h' + (m % 60 ? ' ' + (m % 60) + ' min' : '');
return m + ' min' + (s % 60 ? ' ' + (s % 60) + ' s' : '');
} }
// When it was made and how long that took (the time is only known for makes since it was recorded) function megabytes(kb) { return kb >= 1048576 ? (kb / 1048576).toFixed(1) + ' GB' : kb >= 1024 ? Math.round(kb / 1024) + ' MB' : kb + ' KB'; }
// What the last make cost: its time, the worker's CPU time and the memory the UGC server estimated for it
function costText(i) {
if (!i.processMs) return '';
return 'took ' + duration(i.processMs) + (i.processCpuMs ? ', CPU ' + duration(i.processCpuMs) : '') + (i.processMemoryKb ? ', ~' + megabytes(i.processMemoryKb) + ' RAM (est.)' : '');
}
// When it was made and what that cost (the cost is only known for makes since it was recorded)
function madeText(i) { function madeText(i) {
if (!i.processedAt) return ''; if (!i.processedAt) return '';
return fmt.unix(i.processedAt) + (i.processMs ? ' \u00b7 took ' + duration(i.processMs) : ''); return fmt.unix(i.processedAt) + (costText(i) ? ' \u00b7 ' + costText(i) : '');
} }
function waitBadge(i) { function waitBadge(i) {
return i.state === 'pending' && i.processAfter > Date.now() / 1000 ? ' <span class="small text-body-secondary" title="Waits for the owner to stop saving">after ' + esc(fmt.unix(i.processAfter)) + '</span>' : ''; return i.state === 'pending' && i.processAfter > Date.now() / 1000 ? ' <span class="small text-body-secondary" title="Waits for the owner to stop saving">after ' + esc(fmt.unix(i.processAfter)) + '</span>' : '';
@@ -288,7 +298,7 @@
meter('Jobs\' memory', s.jobMemoryBytes || 0, l.maxMemoryBytes || 0, esc(mb(s.jobMemoryBytes || 0)) + ' estimated' + meter('Jobs\' memory', s.jobMemoryBytes || 0, l.maxMemoryBytes || 0, esc(mb(s.jobMemoryBytes || 0)) + ' estimated' +
(l.maxMemoryBytes ? ' of ' + esc(mb(l.maxMemoryBytes)) : ', no limit') + (s.memoryWaits ? ', ' + esc(s.memoryWaits) + ' waits' : '')) + (l.maxMemoryBytes ? ' of ' + esc(mb(l.maxMemoryBytes)) : ', no limit') + (s.memoryWaits ? ', ' + esc(s.memoryWaits) + ' waits' : '')) +
'</div>' + (l.nice ? '<div class="text-body-secondary">Workers run at priority ' + esc(l.nice) + '.</div>' : '') + '</div>' + (l.nice ? '<div class="text-body-secondary">Workers run at priority ' + esc(l.nice) + '.</div>' : '') +
(last ? '<div class="text-body-secondary">Last: ' + esc(last.kind) + ' ' + esc(last.id) + (last.ok ? ' made in ' + esc(last.ms) + ' ms' : ' failed') + (last ? '<div class="text-body-secondary">Last: ' + esc(last.kind) + ' ' + esc(last.id) + (last.ok ? ' made in ' + esc(duration(last.ms)) : ' failed') +
(last.message ? ' (' + esc(last.message) + ')' : '') + '</div>' : ''); (last.message ? ' (' + esc(last.message) + ')' : '') + '</div>' : '');
}).catch(function () { box.innerHTML = fmt.badge('Unknown', 'secondary') + ' Couldn\'t ask the dashboard for the UGC server\'s status.'; }); }).catch(function () { box.innerHTML = fmt.badge('Unknown', 'secondary') + ' Couldn\'t ask the dashboard for the UGC server\'s status.'; });
} }

View File

@@ -95,8 +95,13 @@ public:
// What the UGC server counted when it made a model: its bricks and the most detailed mesh's triangles // What the UGC server counted when it made a model: its bricks and the most detailed mesh's triangles
virtual void SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) = 0; virtual void SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) = 0;
// How long the last successful make of a model took (process_ms), for the dashboard // The last successful make of a model: how long it took, the worker's CPU time and the estimated memory
virtual void SetUgcModelProcessMs(const LWOOBJID id, const uint32_t milliseconds) = 0; struct ProcessStats {
uint32_t milliseconds{};
uint32_t cpuMilliseconds{};
uint32_t memoryKb{};
};
virtual void SetUgcModelProcessStats(const LWOOBJID id, const ProcessStats& stats) = 0;
virtual std::optional<ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) = 0; virtual std::optional<ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) = 0;

View File

@@ -37,6 +37,8 @@ public:
uint32_t bricks{}; // models: counted by the UGC server when it made them (0: not yet) uint32_t bricks{}; // models: counted by the UGC server when it made them (0: not yet)
uint32_t triangles{}; // models: of the made mesh's most detailed level (0: not yet) uint32_t triangles{}; // models: of the made mesh's most detailed level (0: not yet)
uint32_t processMs{}; // how long the last successful make took (0: not made, or made before it was timed) uint32_t processMs{}; // how long the last successful make took (0: not made, or made before it was timed)
uint32_t processCpuMs{}; // the worker thread's CPU time for it
uint32_t processMemoryKb{}; // the memory the UGC server estimated for it (not measured)
}; };
// What SearchUgc matches. A number (when set) is matched against ids; text against names // What SearchUgc matches. A number (when set) is matched against ids; text against names

View File

@@ -32,8 +32,8 @@ public:
// The combination of modules a build was made as (UgcModularKey::StorageId of its modules), whose files it shares // The combination of modules a build was made as (UgcModularKey::StorageId of its modules), whose files it shares
virtual void SetModularBuildCombination(const LWOOBJID id, const LWOOBJID combinationId) = 0; virtual void SetModularBuildCombination(const LWOOBJID id, const LWOOBJID combinationId) = 0;
// How long the last successful make of a build's icon took (process_ms), for the dashboard // The last successful make of a build's icon (see IUgc::ProcessStats)
virtual void SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) = 0; virtual void SetModularBuildProcessStats(const LWOOBJID id, const IUgc::ProcessStats& stats) = 0;
// Up to `limit` builds whose combination isn't recorded yet (combination_id 0): their ids and modules // Up to `limit` builds whose combination isn't recorded yet (combination_id 0): their ids and modules
virtual std::vector<PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) = 0; virtual std::vector<PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) = 0;

View File

@@ -31,10 +31,10 @@ namespace UgcLookupSql {
return (modular return (modular
? "SELECT b.ugc_id AS id, b.character_id, c.name AS character_name, COALESCE(c.account_id, 0) AS account_id, a.name AS account_name, " ? "SELECT b.ugc_id AS id, b.character_id, c.name AS character_name, COALESCE(c.account_id, 0) AS account_id, a.name AS account_name, "
"b.is_optimized, b.process_error, b.ldf_config AS detail, b.process_attempts, b.processed_at, 0 AS process_after, 0 AS bake_ao, " "b.is_optimized, b.process_error, b.ldf_config AS detail, b.process_attempts, b.processed_at, 0 AS process_after, 0 AS bake_ao, "
"0 AS brick_count, 0 AS triangle_count, b.process_ms " "0 AS brick_count, 0 AS triangle_count, b.process_ms, b.process_cpu_ms, b.process_memory_kb "
: "SELECT u.id, u.character_id, c.name AS character_name, u.account_id, a.name AS account_name, " : "SELECT u.id, u.character_id, c.name AS character_name, u.account_id, a.name AS account_name, "
"u.is_optimized, u.process_error, u.filename AS detail, u.process_attempts, u.processed_at, u.process_after, u.bake_ao, " "u.is_optimized, u.process_error, u.filename AS detail, u.process_attempts, u.processed_at, u.process_after, u.bake_ao, "
"u.brick_count, u.triangle_count, u.process_ms ") + From(modular); "u.brick_count, u.triangle_count, u.process_ms, u.process_cpu_ms, u.process_memory_kb ") + From(modular);
} }
// Whether a search has anything to match (else a list is of everything) // Whether a search has anything to match (else a list is of everything)

View File

@@ -432,8 +432,8 @@ public:
void SetUgcFileChecksum(const eFileOwner owner, const LWOOBJID storageId, const std::string_view file, const std::string_view md5, const uint32_t size) override; void SetUgcFileChecksum(const eFileOwner owner, const LWOOBJID storageId, const std::string_view file, const std::string_view md5, const uint32_t size) override;
std::optional<IUgc::FileChecksum> GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) override; std::optional<IUgc::FileChecksum> GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) override;
void SetModularBuildCombination(const LWOOBJID id, const LWOOBJID combinationId) override; void SetModularBuildCombination(const LWOOBJID id, const LWOOBJID combinationId) override;
void SetUgcModelProcessMs(const LWOOBJID id, const uint32_t milliseconds) override; void SetUgcModelProcessStats(const LWOOBJID id, const ProcessStats& stats) override;
void SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) override; void SetModularBuildProcessStats(const LWOOBJID id, const IUgc::ProcessStats& stats) override;
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) override; std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) override;
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override; std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override;
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override; uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override;

View File

@@ -195,6 +195,6 @@ std::optional<IUgc::FileChecksum> MySQLDatabase::GetUgcFileChecksum(const LWOOBJ
return IUgc::FileChecksum{ std::string(result->getString("md5").c_str()), static_cast<uint32_t>(result->getUInt("size")) }; return IUgc::FileChecksum{ std::string(result->getString("md5").c_str()), static_cast<uint32_t>(result->getUInt("size")) };
} }
void MySQLDatabase::SetUgcModelProcessMs(const LWOOBJID id, const uint32_t milliseconds) { void MySQLDatabase::SetUgcModelProcessStats(const LWOOBJID id, const ProcessStats& stats) {
ExecuteUpdate("UPDATE ugc SET process_ms = ? WHERE id = ?;", milliseconds, id); ExecuteUpdate("UPDATE ugc SET process_ms = ?, process_cpu_ms = ?, process_memory_kb = ? WHERE id = ?;", stats.milliseconds, stats.cpuMilliseconds, stats.memoryKb, id);
} }

View File

@@ -21,6 +21,8 @@ namespace {
entry.bricks = static_cast<uint32_t>(result->getInt64("brick_count")); entry.bricks = static_cast<uint32_t>(result->getInt64("brick_count"));
entry.triangles = static_cast<uint32_t>(result->getInt64("triangle_count")); entry.triangles = static_cast<uint32_t>(result->getInt64("triangle_count"));
entry.processMs = static_cast<uint32_t>(result->getInt64("process_ms")); entry.processMs = static_cast<uint32_t>(result->getInt64("process_ms"));
entry.processCpuMs = static_cast<uint32_t>(result->getInt64("process_cpu_ms"));
entry.processMemoryKb = static_cast<uint32_t>(result->getInt64("process_memory_kb"));
return entry; return entry;
} }
} }

View File

@@ -120,6 +120,6 @@ std::vector<IUgcModularBuild::PendingBuild> MySQLDatabase::GetModularBuildsWitho
return builds; return builds;
} }
void MySQLDatabase::SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) { void MySQLDatabase::SetModularBuildProcessStats(const LWOOBJID id, const IUgc::ProcessStats& stats) {
ExecuteUpdate("UPDATE ugc_modular_build SET process_ms = ? WHERE ugc_id = ?;", milliseconds, id); ExecuteUpdate("UPDATE ugc_modular_build SET process_ms = ?, process_cpu_ms = ?, process_memory_kb = ? WHERE ugc_id = ?;", stats.milliseconds, stats.cpuMilliseconds, stats.memoryKb, id);
} }

View File

@@ -416,8 +416,8 @@ public:
void SetUgcFileChecksum(const eFileOwner owner, const LWOOBJID storageId, const std::string_view file, const std::string_view md5, const uint32_t size) override; void SetUgcFileChecksum(const eFileOwner owner, const LWOOBJID storageId, const std::string_view file, const std::string_view md5, const uint32_t size) override;
std::optional<IUgc::FileChecksum> GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) override; std::optional<IUgc::FileChecksum> GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) override;
void SetModularBuildCombination(const LWOOBJID id, const LWOOBJID combinationId) override; void SetModularBuildCombination(const LWOOBJID id, const LWOOBJID combinationId) override;
void SetUgcModelProcessMs(const LWOOBJID id, const uint32_t milliseconds) override; void SetUgcModelProcessStats(const LWOOBJID id, const ProcessStats& stats) override;
void SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) override; void SetModularBuildProcessStats(const LWOOBJID id, const IUgc::ProcessStats& stats) override;
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) override; std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) override;
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override; std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override;
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override; uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override;

View File

@@ -198,6 +198,6 @@ std::optional<IUgc::FileChecksum> SQLiteDatabase::GetUgcFileChecksum(const LWOOB
return IUgc::FileChecksum{ std::string(result.getStringField("md5", "")), static_cast<uint32_t>(result.getInt64Field("size")) }; return IUgc::FileChecksum{ std::string(result.getStringField("md5", "")), static_cast<uint32_t>(result.getInt64Field("size")) };
} }
void SQLiteDatabase::SetUgcModelProcessMs(const LWOOBJID id, const uint32_t milliseconds) { void SQLiteDatabase::SetUgcModelProcessStats(const LWOOBJID id, const ProcessStats& stats) {
ExecuteUpdate("UPDATE ugc SET process_ms = ? WHERE id = ?;", milliseconds, id); ExecuteUpdate("UPDATE ugc SET process_ms = ?, process_cpu_ms = ?, process_memory_kb = ? WHERE id = ?;", stats.milliseconds, stats.cpuMilliseconds, stats.memoryKb, id);
} }

View File

@@ -21,6 +21,8 @@ namespace {
entry.bricks = static_cast<uint32_t>(result.getInt64Field("brick_count")); entry.bricks = static_cast<uint32_t>(result.getInt64Field("brick_count"));
entry.triangles = static_cast<uint32_t>(result.getInt64Field("triangle_count")); entry.triangles = static_cast<uint32_t>(result.getInt64Field("triangle_count"));
entry.processMs = static_cast<uint32_t>(result.getInt64Field("process_ms")); entry.processMs = static_cast<uint32_t>(result.getInt64Field("process_ms"));
entry.processCpuMs = static_cast<uint32_t>(result.getInt64Field("process_cpu_ms"));
entry.processMemoryKb = static_cast<uint32_t>(result.getInt64Field("process_memory_kb"));
return entry; return entry;
} }
} }

View File

@@ -127,6 +127,6 @@ std::vector<IUgcModularBuild::PendingBuild> SQLiteDatabase::GetModularBuildsWith
return builds; return builds;
} }
void SQLiteDatabase::SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) { void SQLiteDatabase::SetModularBuildProcessStats(const LWOOBJID id, const IUgc::ProcessStats& stats) {
ExecuteUpdate("UPDATE ugc_modular_build SET process_ms = ? WHERE ugc_id = ?;", milliseconds, id); ExecuteUpdate("UPDATE ugc_modular_build SET process_ms = ?, process_cpu_ms = ?, process_memory_kb = ? WHERE ugc_id = ?;", stats.milliseconds, stats.cpuMilliseconds, stats.memoryKb, id);
} }

View File

@@ -414,8 +414,8 @@ class TestSQLDatabase : public GameDatabase {
void SetUgcFileChecksum(const eFileOwner owner, const LWOOBJID storageId, const std::string_view file, const std::string_view md5, const uint32_t size) override {} void SetUgcFileChecksum(const eFileOwner owner, const LWOOBJID storageId, const std::string_view file, const std::string_view md5, const uint32_t size) override {}
std::optional<IUgc::FileChecksum> GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) override { return std::nullopt; } std::optional<IUgc::FileChecksum> GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) override { return std::nullopt; }
void SetModularBuildCombination(const LWOOBJID id, const LWOOBJID combinationId) override {} void SetModularBuildCombination(const LWOOBJID id, const LWOOBJID combinationId) override {}
void SetUgcModelProcessMs(const LWOOBJID id, const uint32_t milliseconds) override {} void SetUgcModelProcessStats(const LWOOBJID id, const ProcessStats& stats) override {}
void SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) override {} void SetModularBuildProcessStats(const LWOOBJID id, const IUgc::ProcessStats& stats) override {}
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) override { return {}; } std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsWithoutCombination(const uint32_t limit) override { return {}; }
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override { return {}; } std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override { return {}; }
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override { return 0; } uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override { return 0; }

View File

@@ -221,7 +221,9 @@ void UgcProcessor::Worker() {
continue; continue;
} }
const auto start = std::chrono::steady_clock::now(); const auto start = std::chrono::steady_clock::now();
const double cpuStart = UgcThrottle::ThreadCpuSeconds();
Done done{ job.kind, job.id, job.attempts }; Done done{ job.kind, job.id, job.attempts };
done.memoryEstimate = job.memory;
done.iconOnly = job.iconOnly; done.iconOnly = job.iconOnly;
try { try {
if (job.iconOnly) { if (job.iconOnly) {
@@ -262,6 +264,7 @@ void UgcProcessor::Worker() {
} }
done.outcome.files.clear(); done.outcome.files.clear();
done.milliseconds = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count(); done.milliseconds = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
done.cpuMilliseconds = std::max(0.0, UgcThrottle::ThreadCpuSeconds() - cpuStart) * 1000.0;
UgcThrottle::Checkpoint(); UgcThrottle::Checkpoint();
{ {
@@ -367,6 +370,8 @@ void UgcProcessor::Poll() {
} }
void UgcProcessor::Record(const Done& done) { void UgcProcessor::Record(const Done& done) {
// What the make cost (wall time, the worker's CPU time, the estimated memory), for the dashboard
const IUgc::ProcessStats cost{ static_cast<uint32_t>(done.milliseconds), static_cast<uint32_t>(done.cpuMilliseconds), static_cast<uint32_t>(done.memoryEstimate / 1024) };
auto error = done.outcome.error.substr(0, MAX_ERROR_LENGTH); auto error = done.outcome.error.substr(0, MAX_ERROR_LENGTH);
const auto attempts = done.attempts + 1; const auto attempts = done.attempts + 1;
const auto state = done.outcome.ok ? IUgc::eProcessState::DONE const auto state = done.outcome.ok ? IUgc::eProcessState::DONE
@@ -374,7 +379,7 @@ void UgcProcessor::Record(const Done& done) {
: attempts >= m_Config.maxAttempts ? IUgc::eProcessState::FAILED : IUgc::eProcessState::PENDING; : attempts >= m_Config.maxAttempts ? IUgc::eProcessState::FAILED : IUgc::eProcessState::PENDING;
if (done.kind == Kind::MODEL) { if (done.kind == Kind::MODEL) {
Database::Get()->SetUgcModelProcessed(done.id, state, attempts, error, done.outcome.ok && done.outcome.aoBaked); Database::Get()->SetUgcModelProcessed(done.id, state, attempts, error, done.outcome.ok && done.outcome.aoBaked);
if (done.outcome.ok) Database::Get()->SetUgcModelProcessMs(done.id, static_cast<uint32_t>(done.milliseconds)); if (done.outcome.ok) Database::Get()->SetUgcModelProcessStats(done.id, cost);
// What it counted (stats.json), for sorting on the dashboard // What it counted (stats.json), for sorting on the dashboard
const auto stats = done.outcome.ok && !done.outcome.stats.empty() ? nlohmann::json::parse(done.outcome.stats, nullptr, false) : nlohmann::json(); const auto stats = done.outcome.ok && !done.outcome.stats.empty() ? nlohmann::json::parse(done.outcome.stats, nullptr, false) : nlohmann::json();
if (stats.is_object()) { if (stats.is_object()) {
@@ -384,7 +389,7 @@ void UgcProcessor::Record(const Done& done) {
} }
} else { } else {
Database::Get()->SetModularBuildProcessed(done.id, state, attempts, error); Database::Get()->SetModularBuildProcessed(done.id, state, attempts, error);
if (done.outcome.ok) Database::Get()->SetModularBuildProcessMs(done.id, static_cast<uint32_t>(done.milliseconds)); if (done.outcome.ok) Database::Get()->SetModularBuildProcessStats(done.id, cost);
// Which combination's files it shares, for the worlds' manifest answers // Which combination's files it shares, for the worlds' manifest answers
if (const auto combo = m_ComboOf.find(done.id); combo != m_ComboOf.end()) Database::Get()->SetModularBuildCombination(done.id, combo->second); if (const auto combo = m_ComboOf.find(done.id); combo != m_ComboOf.end()) Database::Get()->SetModularBuildCombination(done.id, combo->second);
} }

View File

@@ -173,6 +173,8 @@ private:
UgcJobs::Outcome outcome; UgcJobs::Outcome outcome;
uint64_t bytes{}; uint64_t bytes{};
double milliseconds{}; double milliseconds{};
double cpuMilliseconds{}; // the worker thread's CPU time for it
uint64_t memoryEstimate{}; // the bytes it was estimated to need (the memory budget's figure)
bool iconOnly{}; bool iconOnly{};
std::vector<Checksum> checksums; // of the files written that the client downloads as sd0 std::vector<Checksum> checksums; // of the files written that the client downloads as sd0
}; };

View File

@@ -0,0 +1,25 @@
/* UGC server: process_cpu_ms is the CPU time the worker thread spent on the last successful make, process_memory_kb
the memory the UGC server estimated for it (what its memory budget counts; not measured), for the dashboard. */
SET @dlu_column = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE ugc ADD COLUMN process_cpu_ms INT UNSIGNED NOT NULL DEFAULT 0', 'DO 0') FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'ugc' AND column_name = 'process_cpu_ms');
PREPARE dlu_column_stmt FROM @dlu_column;
EXECUTE dlu_column_stmt;
DEALLOCATE PREPARE dlu_column_stmt;
SET @dlu_column = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE ugc ADD COLUMN process_memory_kb INT UNSIGNED NOT NULL DEFAULT 0', 'DO 0') FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'ugc' AND column_name = 'process_memory_kb');
PREPARE dlu_column_stmt FROM @dlu_column;
EXECUTE dlu_column_stmt;
DEALLOCATE PREPARE dlu_column_stmt;
SET @dlu_column = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE ugc_modular_build ADD COLUMN process_cpu_ms INT UNSIGNED NOT NULL DEFAULT 0', 'DO 0') FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'ugc_modular_build' AND column_name = 'process_cpu_ms');
PREPARE dlu_column_stmt FROM @dlu_column;
EXECUTE dlu_column_stmt;
DEALLOCATE PREPARE dlu_column_stmt;
SET @dlu_column = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE ugc_modular_build ADD COLUMN process_memory_kb INT UNSIGNED NOT NULL DEFAULT 0', 'DO 0') FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'ugc_modular_build' AND column_name = 'process_memory_kb');
PREPARE dlu_column_stmt FROM @dlu_column;
EXECUTE dlu_column_stmt;
DEALLOCATE PREPARE dlu_column_stmt;

View File

@@ -0,0 +1,5 @@
/* UGC server: the CPU time and estimated memory of each model's and build's last make. See the MySQL migration. */
ALTER TABLE ugc ADD COLUMN process_cpu_ms INTEGER NOT NULL DEFAULT 0;
ALTER TABLE ugc ADD COLUMN process_memory_kb INTEGER NOT NULL DEFAULT 0;
ALTER TABLE ugc_modular_build ADD COLUMN process_cpu_ms INTEGER NOT NULL DEFAULT 0;
ALTER TABLE ugc_modular_build ADD COLUMN process_memory_kb INTEGER NOT NULL DEFAULT 0;

View File

@@ -105,9 +105,9 @@ namespace {
"CREATE TABLE charinfo (id BIGINT PRIMARY KEY, account_id INTEGER, name TEXT);" "CREATE TABLE charinfo (id BIGINT PRIMARY KEY, account_id INTEGER, name TEXT);"
"CREATE TABLE properties (id BIGINT PRIMARY KEY, owner_id BIGINT, name TEXT, zone_id INTEGER);" "CREATE TABLE properties (id BIGINT PRIMARY KEY, owner_id BIGINT, name TEXT, zone_id INTEGER);"
"CREATE TABLE ugc (id INTEGER PRIMARY KEY, account_id INTEGER, character_id BIGINT, is_optimized INTEGER, filename TEXT, process_error TEXT DEFAULT '', " "CREATE TABLE ugc (id INTEGER PRIMARY KEY, account_id INTEGER, character_id BIGINT, is_optimized INTEGER, filename TEXT, process_error TEXT DEFAULT '', "
"process_attempts INTEGER DEFAULT 0, processed_at BIGINT DEFAULT 0, process_after BIGINT DEFAULT 0, bake_ao INTEGER DEFAULT 0, brick_count INTEGER DEFAULT 0, triangle_count INTEGER DEFAULT 0, process_ms INTEGER DEFAULT 0);" "process_attempts INTEGER DEFAULT 0, processed_at BIGINT DEFAULT 0, process_after BIGINT DEFAULT 0, bake_ao INTEGER DEFAULT 0, brick_count INTEGER DEFAULT 0, triangle_count INTEGER DEFAULT 0, process_ms INTEGER DEFAULT 0, process_cpu_ms INTEGER DEFAULT 0, process_memory_kb INTEGER DEFAULT 0);"
"CREATE TABLE ugc_modular_build (ugc_id BIGINT PRIMARY KEY, character_id BIGINT, ldf_config TEXT, is_optimized INTEGER DEFAULT 0, process_error TEXT DEFAULT '', " "CREATE TABLE ugc_modular_build (ugc_id BIGINT PRIMARY KEY, character_id BIGINT, ldf_config TEXT, is_optimized INTEGER DEFAULT 0, process_error TEXT DEFAULT '', "
"process_attempts INTEGER DEFAULT 0, processed_at BIGINT DEFAULT 0, process_ms INTEGER DEFAULT 0);" "process_attempts INTEGER DEFAULT 0, processed_at BIGINT DEFAULT 0, process_ms INTEGER DEFAULT 0, process_cpu_ms INTEGER DEFAULT 0, process_memory_kb INTEGER DEFAULT 0);"
"CREATE TABLE properties_contents (id BIGINT PRIMARY KEY, property_id BIGINT, ugc_id BIGINT, lot INTEGER, model_name TEXT DEFAULT '', model_description TEXT DEFAULT '');" "CREATE TABLE properties_contents (id BIGINT PRIMARY KEY, property_id BIGINT, ugc_id BIGINT, lot INTEGER, model_name TEXT DEFAULT '', model_description TEXT DEFAULT '');"
"INSERT INTO accounts VALUES (1, 'builder'), (2, 'racer');" "INSERT INTO accounts VALUES (1, 'builder'), (2, 'racer');"
"INSERT INTO charinfo VALUES (10, 1, 'Bricky'), (20, 2, 'Speedy');" "INSERT INTO charinfo VALUES (10, 1, 'Bricky'), (20, 2, 'Speedy');"