diff --git a/public/language/en-GB/admin/advanced/cache.json b/public/language/en-GB/admin/advanced/cache.json index 9ca1b983fb..7c9a89d14f 100644 --- a/public/language/en-GB/admin/advanced/cache.json +++ b/public/language/en-GB/admin/advanced/cache.json @@ -1,10 +1,5 @@ { "cache": "Cache", - "post-cache": "Post Cache", - "group-cache": "Group Cache", - "local-cache": "Local Cache", - "object-cache": "Object Cache", - "notification-cache": "Notification Cache", "percent-full": "%1% Full", "post-cache-size": "Post Cache Size", "items-in-cache": "Items in Cache" diff --git a/public/src/admin/advanced/cache.js b/public/src/admin/advanced/cache.js index 4b77cfb42b..06c79a580f 100644 --- a/public/src/admin/advanced/cache.js +++ b/public/src/admin/advanced/cache.js @@ -27,6 +27,46 @@ define('admin/advanced/cache', ['alerts'], function (alerts) { } }); }); + + $(document).on('click', '#cache-table th', function () { + const table = $(this).closest('table'); + const tbody = table.find('tbody'); + const columnIndex = $(this).index(); + + const rows = tbody.find('tr').toArray(); + + // Toggle sort direction + const ascending = !!$(this).data('asc'); + $(this).data('asc', !ascending); + + // Remove sort indicators from all headers + table.find('th i').addClass('invisible'); + + $(this).find('i').removeClass('invisible') + .toggleClass('fa-sort-up', ascending) + .toggleClass('fa-sort-down', !ascending); + + rows.sort(function (a, b) { + const A = $(a).children().eq(columnIndex).text().trim(); + const B = $(b).children().eq(columnIndex).text().trim(); + // Remove thousands separators + const cleanA = A.replace(/,/g, ''); + const cleanB = B.replace(/,/g, ''); + + const numA = parseFloat(cleanA); + const numB = parseFloat(cleanB); + + if (!isNaN(numA) && !isNaN(numB)) { + return ascending ? numA - numB : numB - numA; + } + + return ascending ? + A.localeCompare(B) : + B.localeCompare(A); + }); + + tbody.append(rows); + }); }; return Cache; }); diff --git a/src/cache/lru.js b/src/cache/lru.js index a1dbfbd705..0d08ef9b4a 100644 --- a/src/cache/lru.js +++ b/src/cache/lru.js @@ -5,6 +5,7 @@ module.exports = function (opts) { const os = require('os'); const pubsub = require('../pubsub'); + const tracker = require('./tracker'); // lru-cache@7 deprecations const winston = require('winston'); @@ -162,5 +163,6 @@ module.exports = function (opts) { return lruCache.peek(key); }; + tracker.addCache(opts.name, cache); return cache; }; diff --git a/src/cache/tracker.js b/src/cache/tracker.js new file mode 100644 index 0000000000..43dfe17bb4 --- /dev/null +++ b/src/cache/tracker.js @@ -0,0 +1,59 @@ +'use strict'; + + +const utils = require('../utils'); + +const cacheList = Object.create(null); + +exports.addCache = function (key, cache) { + if (Object.hasOwn(cacheList, key)) { + throw new Error(`[cache/tracker] Cache with key "${key}" already exists. This will overwrite the existing cache.`); + } + cacheList[key] = cache; +}; + +exports.getCacheList = async function (sort = 'hits') { + const result = []; + for (const value of Object.values(cacheList)) { + result.push(getInfo(value, process.uptime())); + } + + result.sort((a, b) => b[sort].replace(/,/g, '') - a[sort].replace(/,/g, '')); + + result.sort(function (a, b) { + const A = a[sort].replace(/,/g, ''); + const B = b[sort].replace(/,/g, ''); + const numA = parseFloat(A); + const numB = parseFloat(B); + + if (!isNaN(numA) && !isNaN(numB)) { + return numB - numA; + } + return B.localeCompare(A); + }); + + return result; +}; + +exports.findCacheByName = function (name) { + return cacheList[name]; +}; + +function getInfo(cache, uptimeInSeconds) { + return { + name: cache.name, + length: cache.length, + max: cache.max, + maxSize: cache.maxSize, + itemCount: cache.itemCount, + percentFull: cache.name === 'post' ? + ((cache.length / cache.maxSize) * 100).toFixed(2) : + ((cache.itemCount / cache.max) * 100).toFixed(2), + hits: utils.addCommas(String(cache.hits)), + hitsPerSecond: (cache.hits / uptimeInSeconds).toFixed(2), + misses: utils.addCommas(String(cache.misses)), + hitRatio: ((cache.hits / (cache.hits + cache.misses) || 0)).toFixed(4), + enabled: cache.enabled, + ttl: cache.ttl, + }; +} \ No newline at end of file diff --git a/src/cache/ttl.js b/src/cache/ttl.js index 61cd4c07f4..a6990c6c6d 100644 --- a/src/cache/ttl.js +++ b/src/cache/ttl.js @@ -7,6 +7,7 @@ module.exports = function (opts) { const chalk = require('chalk'); const pubsub = require('../pubsub'); + const tracker = require('./tracker'); const ttlCache = new TTLCache(opts); if (!opts.name) { @@ -137,5 +138,6 @@ module.exports = function (opts) { return ttlCache.get(key, { updateAgeOnGet: false }); }; + tracker.addCache(opts.name, cache); return cache; }; diff --git a/src/controllers/admin/cache.js b/src/controllers/admin/cache.js index 5c5d71c42b..fa1abe81d4 100644 --- a/src/controllers/admin/cache.js +++ b/src/controllers/admin/cache.js @@ -2,63 +2,24 @@ const cacheController = module.exports; -const utils = require('../../utils'); -const plugins = require('../../plugins'); +const tracker = require('../../cache/tracker'); cacheController.get = async function (req, res) { - const postCache = require('../../posts/cache').getOrCreate(); - const groupCache = require('../../groups').cache; - const { objectCache } = require('../../database'); - const localCache = require('../../cache'); - const { delayCache } = require('../../notifications'); - const uptimeInSeconds = process.uptime(); - function getInfo(cache) { - return { - length: cache.length, - max: cache.max, - maxSize: cache.maxSize, - itemCount: cache.itemCount, - percentFull: cache.name === 'post' ? - ((cache.length / cache.maxSize) * 100).toFixed(2) : - ((cache.itemCount / cache.max) * 100).toFixed(2), - hits: utils.addCommas(String(cache.hits)), - hitsPerSecond: (cache.hits / uptimeInSeconds).toFixed(2), - misses: utils.addCommas(String(cache.misses)), - hitRatio: ((cache.hits / (cache.hits + cache.misses) || 0)).toFixed(4), - enabled: cache.enabled, - ttl: cache.ttl, - }; - } - let caches = { - post: postCache, - group: groupCache, - local: localCache, - notification: delayCache, - }; - if (objectCache) { - caches.object = objectCache; - } - caches = await plugins.hooks.fire('filter:admin.cache.get', caches); - for (const [key, value] of Object.entries(caches)) { - caches[key] = getInfo(value); - } + // force post cache to get created + require('../../posts/cache').getOrCreate(); + + const caches = await tracker.getCacheList(); res.render('admin/advanced/cache', { caches }); }; cacheController.dump = async function (req, res, next) { - let caches = { - post: require('../../posts/cache').getOrCreate(), - object: require('../../database').objectCache, - group: require('../../groups').cache, - local: require('../../cache'), - }; - caches = await plugins.hooks.fire('filter:admin.cache.get', caches); - if (!caches.hasOwnProperty(req.query.name)) { + const foundCache = await tracker.findCacheByName(req.query.name); + if (!foundCache || !foundCache.dump) { return next(); } - const data = JSON.stringify(caches[req.query.name].dump(), null, 4); + const data = JSON.stringify(foundCache.dump(), null, 4); res.setHeader('Content-disposition', `attachment; filename= ${req.query.name}-cache.json`); res.setHeader('Content-type', 'application/json'); res.write(data, (err) => { diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index f55eba45d1..83de1c07c7 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -16,7 +16,7 @@ const cacheCreate = require('../cache/lru'); const utils = require('../utils'); const roomUidCache = cacheCreate({ - name: 'chat:room:uids', + name: 'chat-room-uids', max: 500, ttl: 0, }); diff --git a/src/socket.io/admin/cache.js b/src/socket.io/admin/cache.js index 2ac786c7a8..553e01022d 100644 --- a/src/socket.io/admin/cache.js +++ b/src/socket.io/admin/cache.js @@ -2,32 +2,18 @@ const SocketCache = module.exports; -const db = require('../../database'); -const plugins = require('../../plugins'); +const tracker = require('../../cache/tracker'); SocketCache.clear = async function (socket, data) { - const caches = await getAvailableCaches(); - if (!caches[data.name]) { - return; + const foundCache = await tracker.findCacheByName(data.name); + if (foundCache && foundCache.reset) { + foundCache.reset(); } - caches[data.name].reset(); }; SocketCache.toggle = async function (socket, data) { - const caches = await getAvailableCaches(); - if (!caches[data.name]) { - return; + const foundCache = await tracker.findCacheByName(data.name); + if (foundCache) { + foundCache.enabled = data.enabled; } - caches[data.name].enabled = data.enabled; }; - -async function getAvailableCaches() { - const caches = { - post: require('../../posts/cache').getOrCreate(), - object: db.objectCache, - group: require('../../groups').cache, - local: require('../../cache'), - notification: require('../../notifications').delayCache, - }; - return await plugins.hooks.fire('filter:admin.cache.get', caches); -} \ No newline at end of file diff --git a/src/user/blocks.js b/src/user/blocks.js index aa8425fe96..723ace7628 100644 --- a/src/user/blocks.js +++ b/src/user/blocks.js @@ -8,7 +8,7 @@ const cacheCreate = require('../cache/lru'); module.exports = function (User) { User.blocks = { _cache: cacheCreate({ - name: 'user:blocks', + name: 'user-blocks', max: 100, ttl: 0, }), diff --git a/src/views/admin/advanced/cache.tpl b/src/views/admin/advanced/cache.tpl index d19b9acd39..b503c00ce6 100644 --- a/src/views/admin/advanced/cache.tpl +++ b/src/views/admin/advanced/cache.tpl @@ -11,19 +11,19 @@