feat: track all caches created in acp

closes #13979
This commit is contained in:
Barış Soner Uşaklı
2026-02-12 10:31:18 -05:00
parent 0c2ab23268
commit 9ac507e5b4
10 changed files with 134 additions and 89 deletions

View File

@@ -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"

View File

@@ -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;
});

2
src/cache/lru.js vendored
View File

@@ -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;
};

59
src/cache/tracker.js vendored Normal file
View File

@@ -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,
};
}

2
src/cache/ttl.js vendored
View File

@@ -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;
};

View File

@@ -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) => {

View File

@@ -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,
});

View File

@@ -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);
}

View File

@@ -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,
}),

View File

@@ -11,19 +11,19 @@
<div>
<div class="table-responsive">
<table class="table table-sm text-sm">
<table id="cache-table" class="table table-sm text-sm">
<thead>
<tr>
<td></td>
<td class="text-end">capacity</td>
<td class="text-end">count</td>
<td class="text-end">size</td>
<td class="text-end">hits</td>
<td class="text-end">misses</td>
<td class="text-end">hit ratio</td>
<td class="text-end">hits/sec</td>
<td class="text-end">ttl</td>
<td></td>
<th><a href="#" class="text-reset">name</a> <i class="fa-solid invisible fa-sort-down"></i></th>
<th class="text-end"><a href="#" class="text-reset">capacity</a> <i class="fa-solid invisible fa-sort-down"></i></th>
<th class="text-end"><a href="#" class="text-reset">count</a> <i class="fa-solid invisible fa-sort-down"></i></th>
<th class="text-end"><a href="#" class="text-reset">size</a> <i class="fa-solid invisible fa-sort-down"></i></th>
<th class="text-end"><a href="#" class="text-reset">hits</a> <i class="fa-solid fa-sort-down"></i></th>
<th class="text-end"><a href="#" class="text-reset">misses</a> <i class="fa-solid invisible fa-sort-down"></i></th>
<th class="text-end"><a href="#" class="text-reset">hit ratio</a> <i class="fa-solid invisible fa-sort-down"></i></th>
<th class="text-end"><a href="#" class="text-reset">hits/sec</a> <i class="fa-solid invisible fa-sort-down"></i></th>
<th class="text-end"><a href="#" class="text-reset">ttl</a> <i class="fa-solid invisible fa-sort-down"></i></th>
<th></td>
</tr>
</thead>
<tbody class="text-xs">
@@ -34,7 +34,7 @@
<div class="form-check form-switch text-sm" data-name="{@key}" style="min-height: initial;">
<input class="form-check-input" type="checkbox" {{{if caches.enabled}}}checked{{{end}}}>
</div>
[[admin/advanced/cache:{@key}-cache]]
{./name}
</div>
</td>
<td class="text-end">{./percentFull}%</td>
@@ -58,8 +58,8 @@
<td class="text-end">{./ttl}</td>
<td class="">
<div class="d-flex justify-content-end gap-1">
<a href="{config.relative_path}/api/admin/advanced/cache/dump?name={@key}" class="btn btn-light btn-sm"><i class="fa fa-download"></i></a>
<a class="btn btn-sm btn-danger clear" data-name="{@key}"><i class="fa fa-trash"></i></a>
<a href="{config.relative_path}/api/admin/advanced/cache/dump?name={./name}" class="btn btn-light btn-sm"><i class="fa fa-download"></i></a>
<a class="btn btn-sm btn-danger clear" data-name="{./name}"><i class="fa fa-trash"></i></a>
</div>
</td>
</tr>