mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-03-06 12:31:33 +01:00
@@ -1,10 +1,5 @@
|
|||||||
{
|
{
|
||||||
"cache": "Cache",
|
"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",
|
"percent-full": "%1% Full",
|
||||||
"post-cache-size": "Post Cache Size",
|
"post-cache-size": "Post Cache Size",
|
||||||
"items-in-cache": "Items in Cache"
|
"items-in-cache": "Items in Cache"
|
||||||
|
|||||||
@@ -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;
|
return Cache;
|
||||||
});
|
});
|
||||||
|
|||||||
2
src/cache/lru.js
vendored
2
src/cache/lru.js
vendored
@@ -5,6 +5,7 @@ module.exports = function (opts) {
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
|
||||||
const pubsub = require('../pubsub');
|
const pubsub = require('../pubsub');
|
||||||
|
const tracker = require('./tracker');
|
||||||
|
|
||||||
// lru-cache@7 deprecations
|
// lru-cache@7 deprecations
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
@@ -162,5 +163,6 @@ module.exports = function (opts) {
|
|||||||
return lruCache.peek(key);
|
return lruCache.peek(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tracker.addCache(opts.name, cache);
|
||||||
return cache;
|
return cache;
|
||||||
};
|
};
|
||||||
|
|||||||
59
src/cache/tracker.js
vendored
Normal file
59
src/cache/tracker.js
vendored
Normal 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
2
src/cache/ttl.js
vendored
@@ -7,6 +7,7 @@ module.exports = function (opts) {
|
|||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
|
|
||||||
const pubsub = require('../pubsub');
|
const pubsub = require('../pubsub');
|
||||||
|
const tracker = require('./tracker');
|
||||||
|
|
||||||
const ttlCache = new TTLCache(opts);
|
const ttlCache = new TTLCache(opts);
|
||||||
if (!opts.name) {
|
if (!opts.name) {
|
||||||
@@ -137,5 +138,6 @@ module.exports = function (opts) {
|
|||||||
return ttlCache.get(key, { updateAgeOnGet: false });
|
return ttlCache.get(key, { updateAgeOnGet: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tracker.addCache(opts.name, cache);
|
||||||
return cache;
|
return cache;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,63 +2,24 @@
|
|||||||
|
|
||||||
const cacheController = module.exports;
|
const cacheController = module.exports;
|
||||||
|
|
||||||
const utils = require('../../utils');
|
const tracker = require('../../cache/tracker');
|
||||||
const plugins = require('../../plugins');
|
|
||||||
|
|
||||||
cacheController.get = async function (req, res) {
|
cacheController.get = async function (req, res) {
|
||||||
const postCache = require('../../posts/cache').getOrCreate();
|
// force post cache to get created
|
||||||
const groupCache = require('../../groups').cache;
|
require('../../posts/cache').getOrCreate();
|
||||||
const { objectCache } = require('../../database');
|
|
||||||
const localCache = require('../../cache');
|
const caches = await tracker.getCacheList();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.render('admin/advanced/cache', { caches });
|
res.render('admin/advanced/cache', { caches });
|
||||||
};
|
};
|
||||||
|
|
||||||
cacheController.dump = async function (req, res, next) {
|
cacheController.dump = async function (req, res, next) {
|
||||||
let caches = {
|
const foundCache = await tracker.findCacheByName(req.query.name);
|
||||||
post: require('../../posts/cache').getOrCreate(),
|
if (!foundCache || !foundCache.dump) {
|
||||||
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)) {
|
|
||||||
return next();
|
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-disposition', `attachment; filename= ${req.query.name}-cache.json`);
|
||||||
res.setHeader('Content-type', 'application/json');
|
res.setHeader('Content-type', 'application/json');
|
||||||
res.write(data, (err) => {
|
res.write(data, (err) => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const cacheCreate = require('../cache/lru');
|
|||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
|
|
||||||
const roomUidCache = cacheCreate({
|
const roomUidCache = cacheCreate({
|
||||||
name: 'chat:room:uids',
|
name: 'chat-room-uids',
|
||||||
max: 500,
|
max: 500,
|
||||||
ttl: 0,
|
ttl: 0,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,32 +2,18 @@
|
|||||||
|
|
||||||
const SocketCache = module.exports;
|
const SocketCache = module.exports;
|
||||||
|
|
||||||
const db = require('../../database');
|
const tracker = require('../../cache/tracker');
|
||||||
const plugins = require('../../plugins');
|
|
||||||
|
|
||||||
SocketCache.clear = async function (socket, data) {
|
SocketCache.clear = async function (socket, data) {
|
||||||
const caches = await getAvailableCaches();
|
const foundCache = await tracker.findCacheByName(data.name);
|
||||||
if (!caches[data.name]) {
|
if (foundCache && foundCache.reset) {
|
||||||
return;
|
foundCache.reset();
|
||||||
}
|
}
|
||||||
caches[data.name].reset();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SocketCache.toggle = async function (socket, data) {
|
SocketCache.toggle = async function (socket, data) {
|
||||||
const caches = await getAvailableCaches();
|
const foundCache = await tracker.findCacheByName(data.name);
|
||||||
if (!caches[data.name]) {
|
if (foundCache) {
|
||||||
return;
|
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);
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ const cacheCreate = require('../cache/lru');
|
|||||||
module.exports = function (User) {
|
module.exports = function (User) {
|
||||||
User.blocks = {
|
User.blocks = {
|
||||||
_cache: cacheCreate({
|
_cache: cacheCreate({
|
||||||
name: 'user:blocks',
|
name: 'user-blocks',
|
||||||
max: 100,
|
max: 100,
|
||||||
ttl: 0,
|
ttl: 0,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -11,19 +11,19 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm text-sm">
|
<table id="cache-table" class="table table-sm text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<th><a href="#" class="text-reset">name</a> <i class="fa-solid invisible fa-sort-down"></i></th>
|
||||||
<td class="text-end">capacity</td>
|
<th class="text-end"><a href="#" class="text-reset">capacity</a> <i class="fa-solid invisible fa-sort-down"></i></th>
|
||||||
<td class="text-end">count</td>
|
<th class="text-end"><a href="#" class="text-reset">count</a> <i class="fa-solid invisible fa-sort-down"></i></th>
|
||||||
<td class="text-end">size</td>
|
<th class="text-end"><a href="#" class="text-reset">size</a> <i class="fa-solid invisible fa-sort-down"></i></th>
|
||||||
<td class="text-end">hits</td>
|
<th class="text-end"><a href="#" class="text-reset">hits</a> <i class="fa-solid fa-sort-down"></i></th>
|
||||||
<td class="text-end">misses</td>
|
<th class="text-end"><a href="#" class="text-reset">misses</a> <i class="fa-solid invisible fa-sort-down"></i></th>
|
||||||
<td class="text-end">hit ratio</td>
|
<th class="text-end"><a href="#" class="text-reset">hit ratio</a> <i class="fa-solid invisible fa-sort-down"></i></th>
|
||||||
<td class="text-end">hits/sec</td>
|
<th class="text-end"><a href="#" class="text-reset">hits/sec</a> <i class="fa-solid invisible fa-sort-down"></i></th>
|
||||||
<td class="text-end">ttl</td>
|
<th class="text-end"><a href="#" class="text-reset">ttl</a> <i class="fa-solid invisible fa-sort-down"></i></th>
|
||||||
<td></td>
|
<th></td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-xs">
|
<tbody class="text-xs">
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<div class="form-check form-switch text-sm" data-name="{@key}" style="min-height: initial;">
|
<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}}}>
|
<input class="form-check-input" type="checkbox" {{{if caches.enabled}}}checked{{{end}}}>
|
||||||
</div>
|
</div>
|
||||||
[[admin/advanced/cache:{@key}-cache]]
|
{./name}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">{./percentFull}%</td>
|
<td class="text-end">{./percentFull}%</td>
|
||||||
@@ -58,8 +58,8 @@
|
|||||||
<td class="text-end">{./ttl}</td>
|
<td class="text-end">{./ttl}</td>
|
||||||
<td class="">
|
<td class="">
|
||||||
<div class="d-flex justify-content-end gap-1">
|
<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 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="{@key}"><i class="fa fa-trash"></i></a>
|
<a class="btn btn-sm btn-danger clear" data-name="{./name}"><i class="fa fa-trash"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user