Skip to content

Commit 88cc0d2

Browse files
committed
feat: added performance page
1 parent 14c5e5b commit 88cc0d2

14 files changed

Lines changed: 1156 additions & 13 deletions

File tree

api/performance.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const express = require('ultimate-express');
2+
const { verifyRequest } = require('@middleware/verifyRequest');
3+
const { limiter } = require('@middleware/limiter');
4+
const { getPerformanceStats } = require('@lib/sqlite/performanceStats');
5+
6+
const router = new express.Router();
7+
8+
const PluginName = 'Performance';
9+
const PluginRequirements = [];
10+
const PluginVersion = '0.0.1';
11+
12+
router.get('/', verifyRequest('app.admin.stats.read'), limiter(1), async (req, res) => {
13+
const requestedLimit = Number(req.query?.limit) || 200;
14+
const limit = Math.min(Math.max(requestedLimit, 10), 1000);
15+
const performanceStats = getPerformanceStats(limit, limit);
16+
return res.json({ performanceStats });
17+
});
18+
19+
module.exports = {
20+
router,
21+
PluginName,
22+
PluginRequirements,
23+
PluginVersion,
24+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"PageTitle": "Performance",
3+
"Subtitle": "HTTP-Request-Dauer, DB-Query-Latenz und ein Sankey der API-Routen.",
4+
"Summary": {
5+
"Title": "Datenbank-Latenz",
6+
"Description": "Tageswerte fuer DB-Queries. Die API-Route-Traffic-Grafik ist getrennt darunter.",
7+
"DbMin": "Min",
8+
"DbAvg": "Avg",
9+
"DbP95": "P95",
10+
"DbP99": "P99"
11+
},
12+
"Pages": {
13+
"Title": "Top Seiten",
14+
"Description": "Per Route gemessene Page-Load-Zeiten fuer die HTML-Seiten.",
15+
"Empty": "Noch keine Seitenaufrufe fuer heute vorhanden."
16+
},
17+
"Sankey": {
18+
"Title": "API-Request-Sankey",
19+
"Description": "Breite = Anzahl der Requests pro Route-Pfad. Die Struktur folgt dem /api-Baum.",
20+
"Empty": "Noch keine API-Requests fuer heute vorhanden.",
21+
"Error": "Performance-Daten konnten nicht geladen werden."
22+
},
23+
"Table": {
24+
"Title": "Top API-Routen",
25+
"Description": "Antwortzeiten der API-Routen",
26+
"Empty": "Noch keine API-Routen fuer heute vorhanden.",
27+
"Route": "Route",
28+
"Min": "Min",
29+
"Avg": "Avg",
30+
"P95": "P95",
31+
"P99": "P99"
32+
}
33+
}

config/locales/de/AdminSettings.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@
99
"MemRss": "Arbeitsspeicher (Gesamt)",
1010
"MemHeap": "Heap-Speicher (Nutzung/Gesamt)",
1111
"MemHeapExternalBuffer": "Speicher für C++ | Buffers",
12-
"Storage": "Speicher"
12+
"Storage": "Speicher",
13+
"DBQueryStats": "DB-Query-Statistik",
14+
"PageLoadStats": "Seitenlade-Statistik",
15+
"HttpRequestStats": "HTTP-Request-Statistik",
16+
"Requests": "Messungen",
17+
"Routes": "Routen",
18+
"NoPerformanceData": "Noch keine Performance-Daten vorhanden."
1319
},
1420
"Logs": {
1521
"Button": "Logs anzeigen",
22+
"PerformanceButton": "Performance",
1623
"ModalTitle": "Systemlogs",
1724
"Filters": "Level",
1825
"Loading": "Lade Logs...",

config/locals_map.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ module.exports = {
6868
"Categories",
6969
"Items"
7070
],
71+
"/admin/performance": [
72+
"Admin",
73+
"Error",
74+
"Page",
75+
"Setup",
76+
"Navbar",
77+
"AdminPerformance"
78+
],
7179
"/admin/settings": [
7280
"Admin",
7381
"Error",

lib/logger.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const rememberLog = (levelnumber, text, timestamp = getTimestamp()) => {
120120
memoryLogs.push({
121121
id: ++logSequence,
122122
timestamp,
123+
timestampMs: Date.now(),
123124
level,
124125
text: String(text),
125126
});

lib/sqlite/index.js

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const path = require('node:path');
22
const fs = require('node:fs')
3+
const { performance } = require('node:perf_hooks');
34
const Database = require('better-sqlite3');
5+
const { installPerformanceStats, stopPerformanceStats, isRecordingEnabled, recordDbQueryDuration } = require('./performanceStats');
46

57
if (!fs.existsSync(path.join(__dirname, '..', '..', 'storage'))) {
68
fs.mkdirSync(path.join(__dirname, '..', '..', 'storage'), { recursive: true });
@@ -12,26 +14,67 @@ const db = new Database(path.join(__dirname, '..', '..', 'storage', 'application
1214

1315
db.pragma('journal_mode = WAL');
1416

17+
const originalPrepare = db.prepare.bind(db);
18+
const originalExec = db.exec.bind(db);
19+
20+
db.prepare = function patchedPrepare(sql, ...args) {
21+
const statement = originalPrepare(sql, ...args);
22+
return new Proxy(statement, {
23+
get(target, prop, receiver) {
24+
const value = Reflect.get(target, prop, target);
25+
26+
if (typeof value !== 'function' || !['run', 'get', 'all', 'values'].includes(prop)) {
27+
return value;
28+
}
29+
30+
return (...methodArgs) => {
31+
const startTime = performance.now();
32+
try {
33+
return value.apply(target, methodArgs);
34+
} finally {
35+
if (isRecordingEnabled()) {
36+
recordDbQueryDuration(performance.now() - startTime);
37+
}
38+
}
39+
};
40+
}
41+
});
42+
};
43+
44+
db.exec = function patchedExec(sql) {
45+
const startTime = performance.now();
46+
try {
47+
return originalExec(sql);
48+
} finally {
49+
if (isRecordingEnabled()) {
50+
recordDbQueryDuration(performance.now() - startTime);
51+
}
52+
}
53+
};
54+
55+
installPerformanceStats(db);
56+
1557
/**
1658
* Closes the database connection gracefully, ensuring all data is flushed to disk and WAL files are cleaned up.
1759
*/
1860
function closeDatabases() {
1961
if (db) {
2062
try {
63+
stopPerformanceStats();
2164
db.pragma('wal_checkpoint(TRUNCATE)');
2265
db.unsafeMode(false);
2366
db.pragma('journal_mode = DELETE');
24-
process.log.system('WAL successfully merged and deleted.');
67+
process.log?.system?.('WAL successfully merged and deleted.');
2568

2669
} catch (err) {
2770
console.log(err);
28-
process.log.error('Error during WAL merge:', err);
71+
process.log?.error?.('Error during WAL merge:', err);
2972
} finally {
3073
try {
3174
db.close();
32-
process.log.system('SQLite database closed cleanly');
75+
process.log?.system?.('SQLite database closed cleanly');
3376
} catch (closeErr) {
34-
process.log.error('Failed to close DB handle:', closeErr);
77+
process.log?.error?.('Failed to close DB handle:', closeErr);
3578
}
3679
}
3780
}
@@ -140,4 +183,4 @@ module.exports = {
140183
getDBSize,
141184
vacuumDB,
142185
closeDatabases
143-
};
186+
};

0 commit comments

Comments
 (0)