mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-17 10:12:50 +00:00
feat(ugc): when each creation was made and how long it took, on the UGC page
The UGC server records how long each successful make took (process_ms on ugc and ugc_modular_build; migrations sqlite 73, mysql 90). The UGC page shows it with the time it was made: in the models list's Made column (sortable, slowest first), in the tiles' tooltips, in the item preview and in an assembly's References. The gallery can sort by slowest to make too. Makes from before this are shown without a duration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 } };
|
{ "detail", entry.detail }, { "bricks", entry.bricks }, { "triangles", entry.triangles }, { "processMs", entry.processMs } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -241,7 +241,7 @@ namespace UgcRoutes {
|
|||||||
"A page of player models (kind=model) or of car and rocket assemblies (kind=modular: one per combination of modules, however many builds use "
|
"A page of player models (kind=model) or of car and rocket assemblies (kind=modular: one per combination of modules, however many builds use "
|
||||||
"it). Query: q= (\"state:\", \"kind:\"/\"type:\" (a build type, e.g. build6), \"owner:\", \"account:\", \"property:\", \"name:\", "
|
"it). Query: q= (\"state:\", \"kind:\"/\"type:\" (a build type, e.g. build6), \"owner:\", \"account:\", \"property:\", \"name:\", "
|
||||||
"\"lot:\"/\"module:\" (a LOT or a module's name), \"id:\", or plain text across names, owners and ids), state=, type=, sort=newest|oldest|owner|name|"
|
"\"lot:\"/\"module:\" (a LOT or a module's name), \"id:\", or plain text across names, owners and ids), state=, type=, sort=newest|oldest|owner|name|"
|
||||||
"bricks|triangles (models) or newest|oldest|references|name (assemblies), reverse=1 (the sort's other direction), page= (from 0), size= (1-200). {items, total, page, size, counts, "
|
"bricks|triangles|slowest (models) or newest|oldest|references|name (assemblies), reverse=1 (the sort's other direction), page= (from 0), size= (1-200). {items, total, page, size, counts, "
|
||||||
"kinds, ugcPublicUrl, canManage}",
|
"kinds, ugcPublicUrl, canManage}",
|
||||||
[](HTTPReply& reply, const HTTPContext& context) {
|
[](HTTPReply& reply, const HTTPContext& context) {
|
||||||
const bool modular = QueryValue(context.queryString, "kind") == "modular";
|
const bool modular = QueryValue(context.queryString, "kind") == "modular";
|
||||||
@@ -260,7 +260,7 @@ namespace UgcRoutes {
|
|||||||
query.search = parsed.search;
|
query.search = parsed.search;
|
||||||
query.state = parsed.state;
|
query.state = parsed.state;
|
||||||
static const std::map<std::string, IUgcLookup::eSort> SORTS = { { "newest", IUgcLookup::eSort::NEWEST }, { "oldest", IUgcLookup::eSort::OLDEST },
|
static const std::map<std::string, IUgcLookup::eSort> SORTS = { { "newest", IUgcLookup::eSort::NEWEST }, { "oldest", IUgcLookup::eSort::OLDEST },
|
||||||
{ "owner", IUgcLookup::eSort::OWNER }, { "name", IUgcLookup::eSort::NAME }, { "bricks", IUgcLookup::eSort::BRICKS }, { "triangles", IUgcLookup::eSort::TRIANGLES } };
|
{ "owner", IUgcLookup::eSort::OWNER }, { "name", IUgcLookup::eSort::NAME }, { "bricks", IUgcLookup::eSort::BRICKS }, { "triangles", IUgcLookup::eSort::TRIANGLES }, { "slowest", IUgcLookup::eSort::SLOWEST } };
|
||||||
if (const auto it = SORTS.find(sortText); it != SORTS.end()) query.sort = it->second;
|
if (const auto it = SORTS.find(sortText); it != SORTS.end()) query.sort = it->second;
|
||||||
query.reverse = reverse;
|
query.reverse = reverse;
|
||||||
query.offset = page * size;
|
query.offset = page * size;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
var STATES = { pending: ['Waiting', 'secondary'], done: ['Made', 'success'], failed: ['Failed', 'danger'], empty: ['Empty', 'light'] };
|
var STATES = { pending: ['Waiting', 'secondary'], done: ['Made', 'success'], failed: ['Failed', 'danger'], empty: ['Empty', 'light'] };
|
||||||
var SORTS = {
|
var SORTS = {
|
||||||
model: [['newest', 'Newest'], ['oldest', 'Oldest'], ['bricks', 'Most bricks'], ['triangles', 'Most triangles'], ['owner', 'Owner'], ['name', 'File name']],
|
model: [['newest', 'Newest'], ['oldest', 'Oldest'], ['bricks', 'Most bricks'], ['triangles', 'Most triangles'], ['slowest', 'Slowest to make'], ['owner', 'Owner'], ['name', 'File name']],
|
||||||
modular: [['newest', 'Newest'], ['oldest', 'Oldest'], ['references', 'Most builds'], ['name', 'Name']]
|
modular: [['newest', 'Newest'], ['oldest', 'Oldest'], ['references', 'Most builds'], ['name', 'Name']]
|
||||||
};
|
};
|
||||||
// The list's state, as in the address: ?kind=&q=&state=&type=&sort=&page= (from 1)&view=, and the open item (&item=, &build=)
|
// The list's state, as in the address: ?kind=&q=&state=&type=&sort=&page= (from 1)&view=, and the open item (&item=, &build=)
|
||||||
@@ -89,6 +89,17 @@
|
|||||||
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"
|
||||||
|
function duration(ms) {
|
||||||
|
if (ms < 1000) return ms + ' ms';
|
||||||
|
if (ms < 60000) return (ms / 1000).toFixed(1) + ' s';
|
||||||
|
return Math.floor(ms / 60000) + ' min ' + Math.round((ms % 60000) / 1000) + ' s';
|
||||||
|
}
|
||||||
|
// When it was made and how long that took (the time is only known for makes since it was recorded)
|
||||||
|
function madeText(i) {
|
||||||
|
if (!i.processedAt) return '';
|
||||||
|
return fmt.unix(i.processedAt) + (i.processMs ? ' \u00b7 took ' + duration(i.processMs) : '');
|
||||||
|
}
|
||||||
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>' : '';
|
||||||
}
|
}
|
||||||
@@ -96,7 +107,8 @@
|
|||||||
var title = list.kind === 'modular' ? esc(i.kindLabel || 'Build type ' + i.buildType) : (i.characterName ? esc(i.characterName) : '<span class="text-body-secondary">' + esc(i.characterId) + '</span>');
|
var title = list.kind === 'modular' ? esc(i.kindLabel || 'Build type ' + i.buildType) : (i.characterName ? esc(i.characterName) : '<span class="text-body-secondary">' + esc(i.characterId) + '</span>');
|
||||||
var sub = list.kind === 'modular' ? esc(moduleNames(i)) : esc(i.id);
|
var sub = list.kind === 'modular' ? esc(moduleNames(i)) : esc(i.id);
|
||||||
var extra = list.kind === 'modular' ? ' ' + fmt.badge(i.uses + ' build' + (i.uses === 1 ? '' : 's'), 'info') : waitBadge(i);
|
var extra = list.kind === 'modular' ? ' ' + fmt.badge(i.uses + ' build' + (i.uses === 1 ? '' : 's'), 'info') : waitBadge(i);
|
||||||
return '<div class="card ugc-tile p-2" tabindex="0" role="button" data-preview="' + esc(i.id) + '" title="' + esc(i.error || sub) + '">' + iconImg(i, 128) +
|
var tip = i.error || (list.kind === 'modular' ? moduleNames(i) : i.id) + (madeText(i) ? '\nMade ' + madeText(i) : '');
|
||||||
|
return '<div class="card ugc-tile p-2" tabindex="0" role="button" data-preview="' + esc(i.id) + '" title="' + esc(tip) + '">' + iconImg(i, 128) +
|
||||||
'<div class="small text-truncate mt-1">' + title + '</div><div class="small text-body-secondary text-truncate">' + sub + '</div><div>' + badge(i.state) + extra + '</div></div>';
|
'<div class="small text-truncate mt-1">' + title + '</div><div class="small text-body-secondary text-truncate">' + sub + '</div><div>' + badge(i.state) + extra + '</div></div>';
|
||||||
}
|
}
|
||||||
function actions(i, remakeId) {
|
function actions(i, remakeId) {
|
||||||
@@ -119,7 +131,7 @@
|
|||||||
column('State', function (i) {
|
column('State', function (i) {
|
||||||
return badge(i.state) + (i.attempts ? ' <span class="small text-body-secondary">' + esc(i.attempts) + ' attempt' + (i.attempts === 1 ? '' : 's') + '</span>' : '') + waitBadge(i);
|
return badge(i.state) + (i.attempts ? ' <span class="small text-body-secondary">' + esc(i.attempts) + ' attempt' + (i.attempts === 1 ? '' : 's') + '</span>' : '') + waitBadge(i);
|
||||||
}),
|
}),
|
||||||
column('Made', function (i) { return '<span class="small">' + (i.processedAt ? esc(fmt.unix(i.processedAt)) : '') + '</span>'; }),
|
column('Made', function (i) { return '<span class="small">' + esc(madeText(i)) + '</span>'; }, 'slowest', true),
|
||||||
column('Size', function (i) { return '<span class="small">' + (i.bricks ? esc(i.bricks) + ' bricks<br>' + esc(i.triangles.toLocaleString()) + ' triangles' : '') + '</span>'; }, 'bricks', true),
|
column('Size', function (i) { return '<span class="small">' + (i.bricks ? esc(i.bricks) + ' bricks<br>' + esc(i.triangles.toLocaleString()) + ' triangles' : '') + '</span>'; }, 'bricks', true),
|
||||||
column('File', function (i) { return '<span class="small">' + esc(i.detail || '') + '</span>' + errorText(i); }, 'name'),
|
column('File', function (i) { return '<span class="small">' + esc(i.detail || '') + '</span>' + errorText(i); }, 'name'),
|
||||||
column('', function (i) { return '<div class="text-end text-nowrap">' + actions(i, i.id) + '</div>'; })
|
column('', function (i) { return '<div class="text-end text-nowrap">' + actions(i, i.id) + '</div>'; })
|
||||||
@@ -381,7 +393,7 @@
|
|||||||
'<button type="button" class="btn btn-sm btn-outline-danger" data-delete-open="gone">Delete files, leave deleted</button></div>';
|
'<button type="button" class="btn btn-sm btn-outline-danger" data-delete-open="gone">Delete files, leave deleted</button></div>';
|
||||||
}
|
}
|
||||||
if (item.error) links += '<div class="text-danger mt-2">' + esc(item.error) + '</div>';
|
if (item.error) links += '<div class="text-danger mt-2">' + esc(item.error) + '</div>';
|
||||||
$('previewLinks').innerHTML = links;
|
$('previewLinks').innerHTML = (!modular && madeText(item) ? '<div class="small text-body-secondary mb-1">Made ' + esc(madeText(item)) + '</div>' : '') + links;
|
||||||
['nifColumn', 'lxfmlColumn', 'meshControls'].forEach(function (x) { $(x).classList.toggle('d-none', modular); });
|
['nifColumn', 'lxfmlColumn', 'meshControls'].forEach(function (x) { $(x).classList.toggle('d-none', modular); });
|
||||||
$('statsBox').innerHTML = modular ? '<div class="small mb-1">Modules (combination <code>' + esc(item.key) + '</code>, used by ' + esc(item.uses) + ' build' + (item.uses === 1 ? '' : 's') +
|
$('statsBox').innerHTML = modular ? '<div class="small mb-1">Modules (combination <code>' + esc(item.key) + '</code>, used by ' + esc(item.uses) + ' build' + (item.uses === 1 ? '' : 's') +
|
||||||
'). The game client puts cars and rockets together itself; the UGC server only draws the icon, once per combination.</div>' + modulesHtml(item) : '';
|
'). The game client puts cars and rockets together itself; the UGC server only draws the icon, once per combination.</div>' + modulesHtml(item) : '';
|
||||||
@@ -448,6 +460,7 @@
|
|||||||
column('Owner', owner, 'owner'),
|
column('Owner', owner, 'owner'),
|
||||||
column('Account', function (b) { return b.accountId ? '<a href="/accounts/' + esc(b.accountId) + '">' + esc(b.accountName || b.accountId) + '</a>' : ''; }, 'account'),
|
column('Account', function (b) { return b.accountId ? '<a href="/accounts/' + esc(b.accountId) + '">' + esc(b.accountName || b.accountId) + '</a>' : ''; }, 'account'),
|
||||||
column('State', function (b) { return badge(b.state) + errorText(b); }, 'state'),
|
column('State', function (b) { return badge(b.state) + errorText(b); }, 'state'),
|
||||||
|
column('Made', function (b) { return '<span class="small">' + esc(madeText(b)) + '</span>'; }),
|
||||||
column('Where it is', function (b) { return whereText(b.where); }),
|
column('Where it is', function (b) { return whereText(b.where); }),
|
||||||
column('', function (b) { return '<div class="text-end"><a class="btn btn-sm btn-outline-secondary" href="/ugc_search?q=' + encodeURIComponent('id:' + b.id) + '">Find</a></div>'; })
|
column('', function (b) { return '<div class="text-end"><a class="btn btn-sm btn-outline-secondary" href="/ugc_search?q=' + encodeURIComponent('id:' + b.id) + '">Find</a></div>'; })
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -171,7 +171,7 @@
|
|||||||
<div class="card-header py-2 small fw-semibold d-flex flex-wrap align-items-center gap-2">References <span class="text-body-secondary fw-normal">the builds that use these modules</span>
|
<div class="card-header py-2 small fw-semibold d-flex flex-wrap align-items-center gap-2">References <span class="text-body-secondary fw-normal">the builds that use these modules</span>
|
||||||
<input type="search" class="form-control form-control-sm ms-auto" style="max-width: 16rem" id="refSearch" placeholder="Owner, account, property or id" aria-label="Search the builds"></div>
|
<input type="search" class="form-control form-control-sm ms-auto" style="max-width: 16rem" id="refSearch" placeholder="Owner, account, property or id" aria-label="Search the builds"></div>
|
||||||
<div class="card-body p-2"><table class="table table-sm small align-middle mb-0 w-100" id="refTable">
|
<div class="card-body p-2"><table class="table table-sm small align-middle mb-0 w-100" id="refTable">
|
||||||
<thead><tr><th>Build (blueprint id)</th><th>Owner</th><th>Account</th><th>State</th><th>Where it is</th><th></th></tr></thead></table></div>
|
<thead><tr><th>Build (blueprint id)</th><th>Owner</th><th>Account</th><th>State</th><th>Made</th><th>Where it is</th><th></th></tr></thead></table></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div></div>
|
</div></div>
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ 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
|
||||||
|
virtual void SetUgcModelProcessMs(const LWOOBJID id, const uint32_t milliseconds) = 0;
|
||||||
|
|
||||||
virtual std::optional<ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) = 0;
|
virtual std::optional<ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) = 0;
|
||||||
|
|
||||||
// Sets models back to pending with no attempts: one (`id`), or all of them (`id` nullopt; only the failed ones
|
// Sets models back to pending with no attempts: one (`id`), or all of them (`id` nullopt; only the failed ones
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public:
|
|||||||
bool bakeAo{};
|
bool bakeAo{};
|
||||||
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)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
@@ -60,6 +61,7 @@ public:
|
|||||||
NAME, // models: the upload's file name; modular builds: the modules
|
NAME, // models: the upload's file name; modular builds: the modules
|
||||||
BRICKS, // models: the most bricks first
|
BRICKS, // models: the most bricks first
|
||||||
TRIANGLES, // models: the most triangles first
|
TRIANGLES, // models: the most triangles first
|
||||||
|
SLOWEST, // the longest last make first (process_ms)
|
||||||
};
|
};
|
||||||
|
|
||||||
// A page of one kind: all of them or those matching the search (the same matching as SearchUgc), in a state or any
|
// A page of one kind: all of them or those matching the search (the same matching as SearchUgc), in a state or any
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ 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
|
||||||
|
virtual void SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) = 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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 "
|
"0 AS brick_count, 0 AS triangle_count, b.process_ms "
|
||||||
: "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 ") + From(modular);
|
"u.brick_count, u.triangle_count, u.process_ms ") + 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)
|
||||||
@@ -58,6 +58,7 @@ namespace UgcLookupSql {
|
|||||||
case eSort::NAME: return std::string("ORDER BY ") + (modular ? "b.ldf_config" : "u.filename") + dir(false) + ", " + id + " DESC ";
|
case eSort::NAME: return std::string("ORDER BY ") + (modular ? "b.ldf_config" : "u.filename") + dir(false) + ", " + id + " DESC ";
|
||||||
case eSort::BRICKS: return modular ? "ORDER BY " + id + dir(true) + " " : "ORDER BY u.brick_count" + std::string(dir(true)) + ", u.id DESC ";
|
case eSort::BRICKS: return modular ? "ORDER BY " + id + dir(true) + " " : "ORDER BY u.brick_count" + std::string(dir(true)) + ", u.id DESC ";
|
||||||
case eSort::TRIANGLES: return modular ? "ORDER BY " + id + dir(true) + " " : "ORDER BY u.triangle_count" + std::string(dir(true)) + ", u.id DESC ";
|
case eSort::TRIANGLES: return modular ? "ORDER BY " + id + dir(true) + " " : "ORDER BY u.triangle_count" + std::string(dir(true)) + ", u.id DESC ";
|
||||||
|
case eSort::SLOWEST: return "ORDER BY " + std::string(modular ? "b" : "u") + ".process_ms" + dir(true) + ", " + id + " DESC ";
|
||||||
default: return "ORDER BY " + id + dir(true) + " ";
|
default: return "ORDER BY " + id + dir(true) + " ";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,6 +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 SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) 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;
|
||||||
|
|||||||
@@ -194,3 +194,7 @@ std::optional<IUgc::FileChecksum> MySQLDatabase::GetUgcFileChecksum(const LWOOBJ
|
|||||||
if (!result->next()) return std::nullopt;
|
if (!result->next()) return std::nullopt;
|
||||||
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) {
|
||||||
|
ExecuteUpdate("UPDATE ugc SET process_ms = ? WHERE id = ?;", milliseconds, id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace {
|
|||||||
entry.bakeAo = result->getInt("bake_ao") != 0;
|
entry.bakeAo = result->getInt("bake_ao") != 0;
|
||||||
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"));
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,3 +119,7 @@ std::vector<IUgcModularBuild::PendingBuild> MySQLDatabase::GetModularBuildsWitho
|
|||||||
}
|
}
|
||||||
return builds;
|
return builds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MySQLDatabase::SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) {
|
||||||
|
ExecuteUpdate("UPDATE ugc_modular_build SET process_ms = ? WHERE ugc_id = ?;", milliseconds, id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -416,6 +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 SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) 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;
|
||||||
|
|||||||
@@ -197,3 +197,7 @@ std::optional<IUgc::FileChecksum> SQLiteDatabase::GetUgcFileChecksum(const LWOOB
|
|||||||
if (result.eof()) return std::nullopt;
|
if (result.eof()) return std::nullopt;
|
||||||
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) {
|
||||||
|
ExecuteUpdate("UPDATE ugc SET process_ms = ? WHERE id = ?;", milliseconds, id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace {
|
|||||||
entry.bakeAo = result.getIntField("bake_ao") != 0;
|
entry.bakeAo = result.getIntField("bake_ao") != 0;
|
||||||
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"));
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,3 +126,7 @@ std::vector<IUgcModularBuild::PendingBuild> SQLiteDatabase::GetModularBuildsWith
|
|||||||
}
|
}
|
||||||
return builds;
|
return builds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SQLiteDatabase::SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) {
|
||||||
|
ExecuteUpdate("UPDATE ugc_modular_build SET process_ms = ? WHERE ugc_id = ?;", milliseconds, id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -414,6 +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 SetModularBuildProcessMs(const LWOOBJID id, const uint32_t milliseconds) 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; }
|
||||||
|
|||||||
@@ -368,6 +368,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));
|
||||||
// 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()) {
|
||||||
@@ -377,6 +378,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));
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
|
|||||||
13
migrations/dlu/mysql/90_ugc_process_time.sql
Normal file
13
migrations/dlu/mysql/90_ugc_process_time.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/* UGC server: ugc.process_ms and ugc_modular_build.process_ms are how long the last successful make took, in
|
||||||
|
milliseconds (0 until it has made it), shown on the dashboard's UGC page. */
|
||||||
|
SET @dlu_column = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE ugc ADD COLUMN process_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_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_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_ms');
|
||||||
|
PREPARE dlu_column_stmt FROM @dlu_column;
|
||||||
|
EXECUTE dlu_column_stmt;
|
||||||
|
DEALLOCATE PREPARE dlu_column_stmt;
|
||||||
3
migrations/dlu/sqlite/73_ugc_process_time.sql
Normal file
3
migrations/dlu/sqlite/73_ugc_process_time.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/* UGC server: how long it took to make each model and car or rocket build. See the MySQL migration. */
|
||||||
|
ALTER TABLE ugc ADD COLUMN process_ms INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE ugc_modular_build ADD COLUMN process_ms INTEGER NOT NULL DEFAULT 0;
|
||||||
@@ -105,15 +105,15 @@ 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_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);"
|
||||||
"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_attempts INTEGER DEFAULT 0, processed_at BIGINT DEFAULT 0, process_ms 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');"
|
||||||
"INSERT INTO properties VALUES (500, 10, 'Castle Hill', 1150);"
|
"INSERT INTO properties VALUES (500, 10, 'Castle Hill', 1150);"
|
||||||
"INSERT INTO ugc (id, account_id, character_id, is_optimized, filename, process_error, brick_count, triangle_count) VALUES "
|
"INSERT INTO ugc (id, account_id, character_id, is_optimized, filename, process_error, brick_count, triangle_count, process_ms) VALUES "
|
||||||
"(1000, 1, 10, 1, 'tower.lxfml', '', 40, 900), (1001, 1, 10, 0, 'boat.lxfml', '', 0, 0);"
|
"(1000, 1, 10, 1, 'tower.lxfml', '', 40, 900, 5200), (1001, 1, 10, 0, 'boat.lxfml', '', 0, 0, 0);"
|
||||||
"INSERT INTO ugc_modular_build (ugc_id, character_id, ldf_config, is_optimized, process_error) VALUES (2000, 20, '1:8129+1:8130+1:9330', 1, '');"
|
"INSERT INTO ugc_modular_build (ugc_id, character_id, ldf_config, is_optimized, process_error) VALUES (2000, 20, '1:8129+1:8130+1:9330', 1, '');"
|
||||||
"INSERT INTO properties_contents VALUES (7000, 500, 1000, 14, 'Big tower', 'the tallest');");
|
"INSERT INTO properties_contents VALUES (7000, 500, 1000, 14, 'Big tower', 'the tallest');");
|
||||||
}
|
}
|
||||||
@@ -182,6 +182,11 @@ TEST_F(UgcLookupSqlTests, ListsPagesWithCounts) {
|
|||||||
query.reverse = true; // the oldest first
|
query.reverse = true; // the oldest first
|
||||||
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1000, 1001 }));
|
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1000, 1001 }));
|
||||||
query.reverse = false;
|
query.reverse = false;
|
||||||
|
query.sort = IUgcLookup::eSort::SLOWEST; // the tower took 5.2 s; the boat isn't made
|
||||||
|
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1000, 1001 }));
|
||||||
|
query.reverse = true;
|
||||||
|
EXPECT_EQ(List(query, false).first, (std::vector<int64_t>{ 1001, 1000 }));
|
||||||
|
query.reverse = false;
|
||||||
query.sort = IUgcLookup::eSort::NAME;
|
query.sort = IUgcLookup::eSort::NAME;
|
||||||
query.limit = 1;
|
query.limit = 1;
|
||||||
query.offset = 1;
|
query.offset = 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user