diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index bc2aa2852..54f5578b8 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -191,7 +191,7 @@ namespace { 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 }, { "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) @@ -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 " "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|" - "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}", [](HTTPReply& reply, const HTTPContext& context) { const bool modular = QueryValue(context.queryString, "kind") == "modular"; @@ -260,7 +260,7 @@ namespace UgcRoutes { query.search = parsed.search; query.state = parsed.state; static const std::map 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; query.reverse = reverse; query.offset = page * size; diff --git a/dDashboardServer/static/js/ugc.js b/dDashboardServer/static/js/ugc.js index e226205a5..57395f42a 100644 --- a/dDashboardServer/static/js/ugc.js +++ b/dDashboardServer/static/js/ugc.js @@ -11,7 +11,7 @@ var STATES = { pending: ['Waiting', 'secondary'], done: ['Made', 'success'], failed: ['Failed', 'danger'], empty: ['Empty', 'light'] }; 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']] }; // 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 ''; } 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) { return i.state === 'pending' && i.processAfter > Date.now() / 1000 ? ' after ' + esc(fmt.unix(i.processAfter)) + '' : ''; } @@ -96,7 +107,8 @@ var title = list.kind === 'modular' ? esc(i.kindLabel || 'Build type ' + i.buildType) : (i.characterName ? esc(i.characterName) : '' + esc(i.characterId) + ''); 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); - return '
' + iconImg(i, 128) + + var tip = i.error || (list.kind === 'modular' ? moduleNames(i) : i.id) + (madeText(i) ? '\nMade ' + madeText(i) : ''); + return '
' + iconImg(i, 128) + '
' + title + '
' + sub + '
' + badge(i.state) + extra + '
'; } function actions(i, remakeId) { @@ -119,7 +131,7 @@ column('State', function (i) { return badge(i.state) + (i.attempts ? ' ' + esc(i.attempts) + ' attempt' + (i.attempts === 1 ? '' : 's') + '' : '') + waitBadge(i); }), - column('Made', function (i) { return '' + (i.processedAt ? esc(fmt.unix(i.processedAt)) : '') + ''; }), + column('Made', function (i) { return '' + esc(madeText(i)) + ''; }, 'slowest', true), column('Size', function (i) { return '' + (i.bricks ? esc(i.bricks) + ' bricks
' + esc(i.triangles.toLocaleString()) + ' triangles' : '') + '
'; }, 'bricks', true), column('File', function (i) { return '' + esc(i.detail || '') + '' + errorText(i); }, 'name'), column('', function (i) { return '
' + actions(i, i.id) + '
'; }) @@ -381,7 +393,7 @@ '
'; } if (item.error) links += '
' + esc(item.error) + '
'; - $('previewLinks').innerHTML = links; + $('previewLinks').innerHTML = (!modular && madeText(item) ? '
Made ' + esc(madeText(item)) + '
' : '') + links; ['nifColumn', 'lxfmlColumn', 'meshControls'].forEach(function (x) { $(x).classList.toggle('d-none', modular); }); $('statsBox').innerHTML = modular ? '
Modules (combination ' + esc(item.key) + ', 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.
' + modulesHtml(item) : ''; @@ -448,6 +460,7 @@ column('Owner', owner, 'owner'), column('Account', function (b) { return b.accountId ? '' + esc(b.accountName || b.accountId) + '' : ''; }, 'account'), column('State', function (b) { return badge(b.state) + errorText(b); }, 'state'), + column('Made', function (b) { return '' + esc(madeText(b)) + ''; }), column('Where it is', function (b) { return whereText(b.where); }), column('', function (b) { return '
Find
'; }) ]; diff --git a/dDashboardServer/templates/ugc.jinja2 b/dDashboardServer/templates/ugc.jinja2 index 205ddde6a..76448f37b 100644 --- a/dDashboardServer/templates/ugc.jinja2 +++ b/dDashboardServer/templates/ugc.jinja2 @@ -171,7 +171,7 @@
References the builds that use these modules
-
Build (blueprint id)OwnerAccountStateWhere it is
+ Build (blueprint id)OwnerAccountStateMadeWhere it is diff --git a/dDatabase/GameDatabase/ITables/IUgc.h b/dDatabase/GameDatabase/ITables/IUgc.h index 902719339..dd11b333f 100644 --- a/dDatabase/GameDatabase/ITables/IUgc.h +++ b/dDatabase/GameDatabase/ITables/IUgc.h @@ -95,6 +95,9 @@ public: // 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; + // 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 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 diff --git a/dDatabase/GameDatabase/ITables/IUgcLookup.h b/dDatabase/GameDatabase/ITables/IUgcLookup.h index 6284829cc..e1a3117fa 100644 --- a/dDatabase/GameDatabase/ITables/IUgcLookup.h +++ b/dDatabase/GameDatabase/ITables/IUgcLookup.h @@ -36,6 +36,7 @@ public: bool bakeAo{}; 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 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 @@ -60,6 +61,7 @@ public: NAME, // models: the upload's file name; modular builds: the modules BRICKS, // models: the most bricks 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 diff --git a/dDatabase/GameDatabase/ITables/IUgcModularBuild.h b/dDatabase/GameDatabase/ITables/IUgcModularBuild.h index bb62b9a27..b04c3ff10 100644 --- a/dDatabase/GameDatabase/ITables/IUgcModularBuild.h +++ b/dDatabase/GameDatabase/ITables/IUgcModularBuild.h @@ -32,6 +32,9 @@ public: // 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; + // 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 virtual std::vector GetModularBuildsWithoutCombination(const uint32_t limit) = 0; diff --git a/dDatabase/GameDatabase/ITables/UgcLookupSql.h b/dDatabase/GameDatabase/ITables/UgcLookupSql.h index 20412aee8..06084d68e 100644 --- a/dDatabase/GameDatabase/ITables/UgcLookupSql.h +++ b/dDatabase/GameDatabase/ITables/UgcLookupSql.h @@ -31,10 +31,10 @@ namespace UgcLookupSql { 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, " "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, " "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) @@ -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::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::SLOWEST: return "ORDER BY " + std::string(modular ? "b" : "u") + ".process_ms" + dir(true) + ", " + id + " DESC "; default: return "ORDER BY " + id + dir(true) + " "; } } diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 78c629bbc..fea0b6bc8 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -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; std::optional GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) 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 GetModularBuildsWithoutCombination(const uint32_t limit) override; std::optional GetUgcProcessInfo(const LWOOBJID id) override; uint64_t ResetUgcModelProcessing(const std::optional id, const bool failedOnly) override; diff --git a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp index a2270e15d..cbe17cc43 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp @@ -194,3 +194,7 @@ std::optional MySQLDatabase::GetUgcFileChecksum(const LWOOBJ if (!result->next()) return std::nullopt; return IUgc::FileChecksum{ std::string(result->getString("md5").c_str()), static_cast(result->getUInt("size")) }; } + +void MySQLDatabase::SetUgcModelProcessMs(const LWOOBJID id, const uint32_t milliseconds) { + ExecuteUpdate("UPDATE ugc SET process_ms = ? WHERE id = ?;", milliseconds, id); +} diff --git a/dDatabase/GameDatabase/MySQL/Tables/UgcLookup.cpp b/dDatabase/GameDatabase/MySQL/Tables/UgcLookup.cpp index 4dc2991fc..2a5286d21 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/UgcLookup.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/UgcLookup.cpp @@ -20,6 +20,7 @@ namespace { entry.bakeAo = result->getInt("bake_ao") != 0; entry.bricks = static_cast(result->getInt64("brick_count")); entry.triangles = static_cast(result->getInt64("triangle_count")); + entry.processMs = static_cast(result->getInt64("process_ms")); return entry; } } diff --git a/dDatabase/GameDatabase/MySQL/Tables/UgcModularBuild.cpp b/dDatabase/GameDatabase/MySQL/Tables/UgcModularBuild.cpp index d43ad3dc2..403bdde00 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/UgcModularBuild.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/UgcModularBuild.cpp @@ -119,3 +119,7 @@ std::vector MySQLDatabase::GetModularBuildsWitho } 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); +} diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 507836b95..981075b0f 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -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; std::optional GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) 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 GetModularBuildsWithoutCombination(const uint32_t limit) override; std::optional GetUgcProcessInfo(const LWOOBJID id) override; uint64_t ResetUgcModelProcessing(const std::optional id, const bool failedOnly) override; diff --git a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp index 0a26438b3..72ec262af 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp @@ -197,3 +197,7 @@ std::optional SQLiteDatabase::GetUgcFileChecksum(const LWOOB if (result.eof()) return std::nullopt; return IUgc::FileChecksum{ std::string(result.getStringField("md5", "")), static_cast(result.getInt64Field("size")) }; } + +void SQLiteDatabase::SetUgcModelProcessMs(const LWOOBJID id, const uint32_t milliseconds) { + ExecuteUpdate("UPDATE ugc SET process_ms = ? WHERE id = ?;", milliseconds, id); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/UgcLookup.cpp b/dDatabase/GameDatabase/SQLite/Tables/UgcLookup.cpp index a8682a16f..a9209767a 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/UgcLookup.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/UgcLookup.cpp @@ -20,6 +20,7 @@ namespace { entry.bakeAo = result.getIntField("bake_ao") != 0; entry.bricks = static_cast(result.getInt64Field("brick_count")); entry.triangles = static_cast(result.getInt64Field("triangle_count")); + entry.processMs = static_cast(result.getInt64Field("process_ms")); return entry; } } diff --git a/dDatabase/GameDatabase/SQLite/Tables/UgcModularBuild.cpp b/dDatabase/GameDatabase/SQLite/Tables/UgcModularBuild.cpp index a069d2908..076816270 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/UgcModularBuild.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/UgcModularBuild.cpp @@ -126,3 +126,7 @@ std::vector SQLiteDatabase::GetModularBuildsWith } 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); +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index 6582ac826..5ff358949 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -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 {} std::optional GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) override { return std::nullopt; } 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 GetModularBuildsWithoutCombination(const uint32_t limit) override { return {}; } std::optional GetUgcProcessInfo(const LWOOBJID id) override { return {}; } uint64_t ResetUgcModelProcessing(const std::optional id, const bool failedOnly) override { return 0; } diff --git a/dUgcServer/UgcProcessor.cpp b/dUgcServer/UgcProcessor.cpp index d4dd045c4..f73854ba3 100644 --- a/dUgcServer/UgcProcessor.cpp +++ b/dUgcServer/UgcProcessor.cpp @@ -368,6 +368,7 @@ void UgcProcessor::Record(const Done& done) { : attempts >= m_Config.maxAttempts ? IUgc::eProcessState::FAILED : IUgc::eProcessState::PENDING; if (done.kind == Kind::MODEL) { 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(done.milliseconds)); // 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(); if (stats.is_object()) { @@ -377,6 +378,7 @@ void UgcProcessor::Record(const Done& done) { } } else { Database::Get()->SetModularBuildProcessed(done.id, state, attempts, error); + if (done.outcome.ok) Database::Get()->SetModularBuildProcessMs(done.id, static_cast(done.milliseconds)); // 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); } diff --git a/migrations/dlu/mysql/90_ugc_process_time.sql b/migrations/dlu/mysql/90_ugc_process_time.sql new file mode 100644 index 000000000..5097e1477 --- /dev/null +++ b/migrations/dlu/mysql/90_ugc_process_time.sql @@ -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; diff --git a/migrations/dlu/sqlite/73_ugc_process_time.sql b/migrations/dlu/sqlite/73_ugc_process_time.sql new file mode 100644 index 000000000..504e195c4 --- /dev/null +++ b/migrations/dlu/sqlite/73_ugc_process_time.sql @@ -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; diff --git a/tests/dWebTests/UgcLookupTests.cpp b/tests/dWebTests/UgcLookupTests.cpp index bb33f559f..6c584d6ac 100644 --- a/tests/dWebTests/UgcLookupTests.cpp +++ b/tests/dWebTests/UgcLookupTests.cpp @@ -105,15 +105,15 @@ namespace { "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 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 '', " - "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 '');" "INSERT INTO accounts VALUES (1, 'builder'), (2, 'racer');" "INSERT INTO charinfo VALUES (10, 1, 'Bricky'), (20, 2, 'Speedy');" "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 " - "(1000, 1, 10, 1, 'tower.lxfml', '', 40, 900), (1001, 1, 10, 0, 'boat.lxfml', '', 0, 0);" + "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, 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 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 EXPECT_EQ(List(query, false).first, (std::vector{ 1000, 1001 })); 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{ 1000, 1001 })); + query.reverse = true; + EXPECT_EQ(List(query, false).first, (std::vector{ 1001, 1000 })); + query.reverse = false; query.sort = IUgcLookup::eSort::NAME; query.limit = 1; query.offset = 1;