Compare commits
20 commits
main
...
loadsodium
Author | SHA1 | Date | |
---|---|---|---|
|
bc6765d9f6 | ||
|
43cfc926bb | ||
|
1d639a7653 | ||
|
1310979994 | ||
|
2de0a0d4b9 | ||
|
8c6acf9578 | ||
|
73891d819e | ||
|
83dd6fa16c | ||
|
a8e03cef9d | ||
|
c17312c3fb | ||
|
cf153b8474 | ||
|
eb1249b6cd | ||
|
332edec162 | ||
|
bde6ef8044 | ||
|
9b8e487b70 | ||
|
99f1cf650e | ||
|
9c22a25ba2 | ||
|
fff4e7581e | ||
|
49af0533b5 | ||
|
4f2a48a72e |
14 changed files with 1411 additions and 1211 deletions
44
customize.dist/src/less2/pages/page-load.less
Normal file
44
customize.dist/src/less2/pages/page-load.less
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
@import (reference) "../include/infopages.less";
|
||||
@import (reference) "../include/colortheme-all.less";
|
||||
@import (reference) "../include/alertify.less";
|
||||
@import (reference) "../include/checkmark.less";
|
||||
@import (reference) "../include/forms.less";
|
||||
|
||||
&.cp-page-load {
|
||||
.infopages_main();
|
||||
.forms_main();
|
||||
|
||||
.alertify_main();
|
||||
.checkmark_main(20px);
|
||||
|
||||
.form {
|
||||
max-width: 400px;
|
||||
padding: 20px;
|
||||
}
|
||||
.alertify {
|
||||
// workaround for alertify making empty p
|
||||
p:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
nav .btn-danger {
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ nThen(function (w) {
|
|||
};
|
||||
|
||||
// spawn ws server and attach netflux event handlers
|
||||
let Server = NetfluxSrv.create(new WebSocketServer({ server: Env.httpServer}))
|
||||
let Server = Env.Server = NetfluxSrv.create(new WebSocketServer({ server: Env.httpServer}))
|
||||
.on('channelClose', historyKeeper.channelClose)
|
||||
.on('channelMessage', historyKeeper.channelMessage)
|
||||
.on('channelOpen', historyKeeper.channelOpen)
|
||||
|
|
|
@ -254,6 +254,7 @@ module.exports.create = function (config) {
|
|||
curvePublic: Nacl.util.encodeBase64(curve.publicKey),
|
||||
|
||||
selfDestructTo: {},
|
||||
monitoring: {}
|
||||
};
|
||||
|
||||
(function () {
|
||||
|
@ -417,6 +418,7 @@ const BAD = [
|
|||
'limits',
|
||||
'customLimits',
|
||||
'scheduleDecree',
|
||||
'monitoring',
|
||||
|
||||
'httpServer',
|
||||
|
||||
|
|
|
@ -19,6 +19,9 @@ const BlobStore = require("./storage/blob");
|
|||
const BlockStore = require("./storage/block");
|
||||
const plugins = require("./plugin-manager");
|
||||
|
||||
const Prometheus = require('prom-client');
|
||||
const Monitoring = require('./monitoring');
|
||||
|
||||
const DEFAULT_QUERY_TIMEOUT = 5000;
|
||||
const PID = process.pid;
|
||||
|
||||
|
@ -66,6 +69,102 @@ Env.incrementBytesWritten = function () {};
|
|||
|
||||
const EVENTS = {};
|
||||
|
||||
// XXX Store in monitoring.js
|
||||
const rssMetric = new Prometheus.Gauge({
|
||||
name: `memory_rss`,
|
||||
help: 'The amount of space occupied in the main memory device for the process.',
|
||||
labelNames: ['pid', 'type']
|
||||
});
|
||||
const heapTotalMetric = new Prometheus.Gauge({
|
||||
name: `memory_heap_total`,
|
||||
help: "Total heap memory.",
|
||||
labelNames: ['pid', 'type']
|
||||
});
|
||||
const heapUsedMetric = new Prometheus.Gauge({
|
||||
name: `memory_heap_used`,
|
||||
help: 'Used heap memory.',
|
||||
labelNames: ['pid', 'type']
|
||||
});
|
||||
const externalMetric = new Prometheus.Gauge({
|
||||
name: `memory_external`,
|
||||
help: 'Memory usage of C++ objects bound to JavaScript objects managed by V8.',
|
||||
labelNames: ['pid', 'type']
|
||||
});
|
||||
const arrayBufferMetric = new Prometheus.Gauge({
|
||||
name: `memory_array_buffers`,
|
||||
help: 'Memory allocated for ArrayBuffers and SharedArrayBuffers.',
|
||||
labelNames: ['pid', 'type']
|
||||
});
|
||||
const cpuUserMetric = new Prometheus.Gauge({
|
||||
name: `process_cpu_user_seconds_total`,
|
||||
help: 'Total user CPU time spent in seconds during the configured interval.',
|
||||
labelNames: ['pid', 'type']
|
||||
});
|
||||
const cpuSystemMetric = new Prometheus.Gauge({
|
||||
name: `process_cpu_system_seconds_total`,
|
||||
help: 'Total system CPU time spent in seconds during the configured interval.',
|
||||
labelNames: ['pid', 'type']
|
||||
});
|
||||
const cpuTotalMetric = new Prometheus.Gauge({
|
||||
name: `process_cpu_seconds_total`,
|
||||
help: 'Total user and system CPU time spent in seconds during the configured interval',
|
||||
labelNames: ['pid', 'type']
|
||||
});
|
||||
const cpuPercentMetric = new Prometheus.Gauge({
|
||||
name: `process_cpu_percent`,
|
||||
help: 'Total user and system CPU time spent divided by the interval duration',
|
||||
labelNames: ['pid', 'type']
|
||||
});
|
||||
const wsMetric = new Prometheus.Gauge({
|
||||
name: `active_websockets`,
|
||||
help: 'Number of active websocket connections',
|
||||
});
|
||||
const regMetric = new Prometheus.Gauge({
|
||||
name: `active_registered_users`,
|
||||
help: 'Number of registered users online',
|
||||
});
|
||||
const chanMetric = new Prometheus.Gauge({
|
||||
name: `active_channels`,
|
||||
help: 'Number of active pads',
|
||||
});
|
||||
EVENTS.MONITORING = function (data) {
|
||||
/*
|
||||
{
|
||||
main: {
|
||||
rss: 1234
|
||||
...
|
||||
},
|
||||
pid1: {
|
||||
rss: 234
|
||||
...
|
||||
}
|
||||
}
|
||||
*/
|
||||
Object.keys(data).forEach(pid => {
|
||||
let val = data[pid];
|
||||
let type = val.type;
|
||||
rssMetric.set({pid, type}, val.mem?.rss || 0);
|
||||
heapTotalMetric.set({pid, type}, val.mem?.heapTotal || 0);
|
||||
heapUsedMetric.set({pid, type}, val.mem?.heapUsed || 0);
|
||||
externalMetric.set({pid, type}, val.mem?.external || 0);
|
||||
arrayBufferMetric.set({pid, type}, val.mem?.arrayBuffers || 0);
|
||||
let userSeconds = (val.cpu?.user || 0) / 1000000;
|
||||
let systemSeconds = (val.cpu?.system || 0) / 1000000;
|
||||
cpuUserMetric.set({pid, type}, userSeconds);
|
||||
cpuSystemMetric.set({pid, type}, systemSeconds);
|
||||
let sum = userSeconds + systemSeconds;
|
||||
let percent = sum / (Monitoring.interval/1000);
|
||||
cpuTotalMetric.set({pid, type}, sum);
|
||||
cpuPercentMetric.set({pid, type}, percent);
|
||||
if (type === 'main') {
|
||||
wsMetric.set(val.ws || 0);
|
||||
regMetric.set(val.registered || 0);
|
||||
chanMetric.set(val.channels || 0);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
EVENTS.ENV_UPDATE = function (data /*, cb */) {
|
||||
try {
|
||||
Env = JSON.parse(data);
|
||||
|
@ -212,6 +311,13 @@ const wsProxy = createProxyMiddleware({
|
|||
|
||||
app.use('/cryptpad_websocket', wsProxy);
|
||||
|
||||
app.get('/metrics', (req, res) => {
|
||||
Prometheus.register.metrics().then((data) => {
|
||||
res.set('Content-Type', Prometheus.register.contentType);
|
||||
res.send(data);
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/ssoauth', (req, res, next) => {
|
||||
if (SSOUtils && req && req.body && req.body.SAMLResponse) {
|
||||
req.method = 'GET';
|
||||
|
@ -757,7 +863,14 @@ nThen(function (w) {
|
|||
}));
|
||||
}).nThen(function () {
|
||||
// TODO inform the parent process that this worker is ready
|
||||
|
||||
setInterval(() => {
|
||||
sendMessage({
|
||||
command: 'MONITORING',
|
||||
data: Monitoring.getData('http-worker')
|
||||
}, () => {
|
||||
// Done
|
||||
});
|
||||
}, Monitoring.interval);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function (err) {
|
||||
|
|
45
lib/monitoring.js
Normal file
45
lib/monitoring.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
globals process
|
||||
*/
|
||||
const VALUES = {};
|
||||
VALUES.mem = () => {
|
||||
return process.memoryUsage();
|
||||
};
|
||||
let oldCpu;
|
||||
VALUES.cpu = () => {
|
||||
if (!oldCpu) {
|
||||
oldCpu = process.cpuUsage();
|
||||
return {user:0,system:0};
|
||||
}
|
||||
let val = process.cpuUsage(oldCpu);
|
||||
oldCpu = process.cpuUsage();
|
||||
return val;
|
||||
};
|
||||
|
||||
const applyToEnv = (Env, data) => {
|
||||
if (!Env) { return; }
|
||||
Env.monitoring[data.pid] = data;
|
||||
};
|
||||
const getData = (type) => {
|
||||
const value = {
|
||||
pid: process.pid,
|
||||
type: type
|
||||
};
|
||||
Object.keys(VALUES).forEach(key => {
|
||||
value[key] = VALUES[key]();
|
||||
});
|
||||
return value;
|
||||
};
|
||||
|
||||
const remove = (Env, pid) => {
|
||||
if (Env && Env.monitoring && pid && Env.monitoring[pid]) {
|
||||
delete Env.monitoring[pid];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
interval: 5000,
|
||||
applyToEnv,
|
||||
getData,
|
||||
remove
|
||||
};
|
|
@ -170,6 +170,15 @@ var rpc = function (Env, Server, userId, data, respond) {
|
|||
|
||||
var command = msg[1];
|
||||
|
||||
/*
|
||||
// TODO get data from lib/upload.js to be able to get the size of the uploaded file
|
||||
if (command === 'UPLOAD_COMPLETE' || command === 'OWNED_UPLOAD_COMPLETE') {
|
||||
let m = Env.monitoring = Env.monitoring || {};
|
||||
let b = m.upload = m.upload || {};
|
||||
let id = msg[2];
|
||||
if (id) { b[id] = +new Date(); }
|
||||
}
|
||||
*/
|
||||
if (command === 'UPLOAD') {
|
||||
// UPLOAD is a special case that skips signature validation
|
||||
// intentional fallthrough behaviour
|
||||
|
|
|
@ -17,7 +17,12 @@ const Saferphore = require("saferphore");
|
|||
const Logger = require("../log");
|
||||
const Tasks = require("../storage/tasks");
|
||||
const Nacl = require('tweetnacl/nacl-fast');
|
||||
const Sodium = require('sodium');
|
||||
const LW = require('libsodium-wrappers');
|
||||
const Eviction = require("../eviction");
|
||||
const Monitoring = require('../monitoring');
|
||||
const withSodium = false;
|
||||
const withLibSodium = true;
|
||||
|
||||
const Env = {
|
||||
Log: {},
|
||||
|
@ -58,6 +63,13 @@ const init = function (config, _cb) {
|
|||
Env.archiveRetentionTime = config.archiveRetentionTime;
|
||||
Env.accountRetentionTime = config.accountRetentionTime;
|
||||
|
||||
setInterval(() => {
|
||||
process.send({
|
||||
monitoring: true,
|
||||
data: Monitoring.getData('db-worker')
|
||||
});
|
||||
}, Monitoring.interval);
|
||||
|
||||
nThen(function (w) {
|
||||
Store.create(config, w(function (err, _store) {
|
||||
if (err) {
|
||||
|
@ -681,10 +693,28 @@ COMMANDS.INLINE = function (data, cb) {
|
|||
} catch (e) {
|
||||
return void cb("E_BADKEY");
|
||||
}
|
||||
// validate the message
|
||||
const validated = Nacl.sign.open(signedMsg, validateKey);
|
||||
if (!validated) {
|
||||
if (!withSodium && !withLibSodium) {
|
||||
const validated = Nacl.sign.open(signedMsg, validateKey);
|
||||
if (!validated) {
|
||||
return void cb("FAILED");
|
||||
}
|
||||
} else if (withLibSodium) {
|
||||
const validated = LW.crypto_sign_open(signedMsg, validateKey);
|
||||
if (!validated) {
|
||||
console.log("validation failed");
|
||||
return void cb("failed");
|
||||
}
|
||||
} else {
|
||||
var input = {
|
||||
sign: signedMsg,
|
||||
publicKey: validateKey
|
||||
};
|
||||
|
||||
const validated = Sodium.Sign.verify(input);
|
||||
if (!validated) {
|
||||
console.log("validation failed");
|
||||
return void cb("failed");
|
||||
}
|
||||
}
|
||||
cb();
|
||||
};
|
||||
|
@ -722,8 +752,30 @@ const checkDetachedSignature = function (signedMsg, signature, publicKey) {
|
|||
throw new Error("INVALID_SIGNATURE_LENGTH");
|
||||
}
|
||||
|
||||
if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
|
||||
if (!withSodium && !withLibSodium) {
|
||||
if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
|
||||
throw new Error("FAILED");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let sign = Buffer.concat([ signatureBuffer, signedBuffer]);
|
||||
var input = {
|
||||
sign,
|
||||
publicKey: pubBuffer
|
||||
};
|
||||
if (withLibSodium) {
|
||||
const validated = LW.crypto_sign_open(sign, pubBuffer);
|
||||
if (!validated) {
|
||||
throw new Error('FAILED');
|
||||
}
|
||||
} else {
|
||||
if (!Sodium.Sign.verify(input)) {
|
||||
throw new Error("FAILED");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("SODIUM FAILED");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ const { fork } = require('child_process');
|
|||
const Workers = module.exports;
|
||||
const PID = process.pid;
|
||||
const Block = require("../storage/block");
|
||||
const Monitoring = require('../monitoring');
|
||||
|
||||
const DB_PATH = 'lib/workers/db-worker';
|
||||
const MAX_JOBS = 16;
|
||||
|
@ -163,6 +164,13 @@ Workers.initialize = function (Env, config, _cb) {
|
|||
if (res.log) {
|
||||
return void handleLog(res.log, res.label, res.info);
|
||||
}
|
||||
|
||||
// handle monitoring data
|
||||
if (res.monitoring) {
|
||||
Monitoring.applyToEnv(Env, res.data);
|
||||
return;
|
||||
}
|
||||
|
||||
// but don't bother handling things addressed to other processes
|
||||
// since it's basically guaranteed not to work
|
||||
if (res.pid !== PID) {
|
||||
|
@ -227,7 +235,9 @@ Workers.initialize = function (Env, config, _cb) {
|
|||
handleResponse(state, res);
|
||||
});
|
||||
|
||||
let pid = worker.pid;
|
||||
var substituteWorker = Util.once(function () {
|
||||
Monitoring.remove(Env, pid);
|
||||
Env.Log.info("SUBSTITUTE_DB_WORKER", '');
|
||||
var idx = workers.indexOf(state);
|
||||
if (idx !== -1) {
|
||||
|
@ -264,9 +274,10 @@ Workers.initialize = function (Env, config, _cb) {
|
|||
};
|
||||
|
||||
nThen(function (w) {
|
||||
var limit = Env.maxWorkers;
|
||||
var limit = Env.maxWorkers || OS.cpus().length;
|
||||
var logged;
|
||||
|
||||
/*
|
||||
OS.cpus().forEach(function (cpu, index) {
|
||||
if (limit && index >= limit) {
|
||||
if (!logged) {
|
||||
|
@ -282,6 +293,14 @@ Workers.initialize = function (Env, config, _cb) {
|
|||
return void cb(err);
|
||||
}));
|
||||
});
|
||||
*/
|
||||
for (let i = 0; i<limit; i++) {
|
||||
initWorker(fork(DB_PATH), w(function (err) {
|
||||
if (!err) { return; }
|
||||
w.abort();
|
||||
return void cb(err);
|
||||
}));
|
||||
}
|
||||
}).nThen(function () {
|
||||
Env.computeIndex = function (Env, channel, cb) {
|
||||
Env.store.getWeakLock(channel, function (next) {
|
||||
|
|
1841
package-lock.json
generated
1841
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
@ -22,7 +22,7 @@
|
|||
"chainpad-crypto": "^0.2.5",
|
||||
"chainpad-listmap": "^1.0.0",
|
||||
"chainpad-netflux": "^1.0.0",
|
||||
"chainpad-server": "^5.2.0",
|
||||
"chainpad-server": "^5.2.1",
|
||||
"ckeditor": "npm:ckeditor4@~4.22.1",
|
||||
"codemirror": "^5.19.0",
|
||||
"components-font-awesome": "^4.6.3",
|
||||
|
@ -41,6 +41,7 @@
|
|||
"json.sortify": "~2.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"localforage": "^1.5.2",
|
||||
"marked": "^4.3.0",
|
||||
"mathjax": "3.0.5",
|
||||
|
@ -50,6 +51,7 @@
|
|||
"open-sans-fontface": "^1.4.0",
|
||||
"openid-client": "^5.4.2",
|
||||
"pako": "^2.1.0",
|
||||
"prom-client": "^14.2.0",
|
||||
"prompt-confirm": "^2.0.4",
|
||||
"pull-stream": "^3.6.1",
|
||||
"require-css": "0.1.10",
|
||||
|
@ -57,6 +59,7 @@
|
|||
"requirejs-plugins": "^1.0.2",
|
||||
"saferphore": "0.0.1",
|
||||
"scrypt-async": "1.2.0",
|
||||
"sodium": "^3.0.2",
|
||||
"sortablejs": "^1.6.0",
|
||||
"sortify": "^1.0.4",
|
||||
"stream-to-pull-stream": "^1.7.2",
|
||||
|
@ -82,6 +85,7 @@
|
|||
"install:components": "node scripts/copy-components.js",
|
||||
"start": "node server.js",
|
||||
"dev": "DEV=1 node server.js",
|
||||
"prof": "NODE_ENV=production node --prof server.js",
|
||||
"fresh": "FRESH=1 node server.js",
|
||||
"offline": "FRESH=1 OFFLINE=1 node server.js",
|
||||
"offlinedev": "DEV=1 OFFLINE=1 node server.js",
|
||||
|
@ -98,5 +102,7 @@
|
|||
"clear": "node scripts/clear.js",
|
||||
"installtoken": "node scripts/install.js"
|
||||
},
|
||||
"browserslist": ["> 0.5%, last 2 versions, Firefox ESR, not dead, not op_mini all"]
|
||||
"browserslist": [
|
||||
"> 0.5%, last 2 versions, Firefox ESR, not dead, not op_mini all"
|
||||
]
|
||||
}
|
||||
|
|
21
server.js
21
server.js
|
@ -18,6 +18,7 @@ var config = require("./lib/load-config");
|
|||
var Environment = require("./lib/env");
|
||||
var Env = Environment.create(config);
|
||||
var Default = require("./lib/defaults");
|
||||
var Monitoring = require('./lib/monitoring');
|
||||
|
||||
var app = Express();
|
||||
|
||||
|
@ -52,6 +53,11 @@ COMMANDS.GET_PROFILING_DATA = function (msg, cb) {
|
|||
cb(void 0, Env.bytesWritten);
|
||||
};
|
||||
|
||||
COMMANDS.MONITORING = function (msg, cb) {
|
||||
Monitoring.applyToEnv(Env, msg.data);
|
||||
cb();
|
||||
};
|
||||
|
||||
nThen(function (w) {
|
||||
require("./lib/log").create(config, w(function (_log) {
|
||||
Env.Log = _log;
|
||||
|
@ -103,6 +109,7 @@ nThen(function (w) {
|
|||
|
||||
var launchWorker = (online) => {
|
||||
var worker = Cluster.fork(workerState);
|
||||
var pid = worker.process.pid;
|
||||
worker.on('online', () => {
|
||||
online();
|
||||
});
|
||||
|
@ -132,6 +139,7 @@ nThen(function (w) {
|
|||
});
|
||||
|
||||
worker.on('exit', (code, signal) => {
|
||||
Monitoring.remove(Env, pid);
|
||||
if (!signal && code === 0) { return; }
|
||||
// relaunch http workers if they crash
|
||||
Env.Log.error('HTTP_WORKER_EXIT', {
|
||||
|
@ -173,6 +181,19 @@ nThen(function (w) {
|
|||
broadcast('FLUSH_CACHE', Env.FRESH_KEY);
|
||||
}, 250);
|
||||
|
||||
setInterval(() => {
|
||||
// Add main process data to monitoring
|
||||
let monitoring = Monitoring.getData('main');
|
||||
let Server = Env.Server;
|
||||
let stats = Server.getSessionStats();
|
||||
monitoring.ws = stats.total;
|
||||
monitoring.channels = Server.getActiveChannelCount();
|
||||
monitoring.registered = Object.keys(Env.netfluxUsers).length;
|
||||
// Send updated values
|
||||
Monitoring.applyToEnv(Env, monitoring);
|
||||
broadcast('MONITORING', Env.monitoring);
|
||||
}, Monitoring.interval);
|
||||
|
||||
Env.envUpdated.reg(throttledEnvChange);
|
||||
Env.cacheFlushed.reg(throttledCacheFlush);
|
||||
|
||||
|
|
|
@ -789,7 +789,8 @@ define([
|
|||
|
||||
ownedPads.forEach(function (c) {
|
||||
var w = waitFor();
|
||||
sem.take(function (give) {
|
||||
sem.take(function (_give) {
|
||||
var give = _give();
|
||||
var otherOwners = false;
|
||||
nThen(function (_w) {
|
||||
// Don't check server metadata for blobs
|
||||
|
|
18
www/loadtest/index.html
Normal file
18
www/loadtest/index.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title data-localization="main_title">CryptPad: Collaboration suite, encrypted and open-source</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
|
||||
<script async data-bootload="/loadtest/main.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html cp-page-load">
|
||||
<noscript></noscript>
|
||||
|
431
www/loadtest/main.js
Normal file
431
www/loadtest/main.js
Normal file
|
@ -0,0 +1,431 @@
|
|||
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
define([
|
||||
'/api/config',
|
||||
'jquery',
|
||||
'netflux-client',
|
||||
'/common/hyperscript.js',
|
||||
'/common/common-hash.js',
|
||||
'/common/common-util.js',
|
||||
'/common/common-interface.js',
|
||||
'/common/outer/network-config.js',
|
||||
'/components/nthen/index.js',
|
||||
'/components/saferphore/index.js',
|
||||
'/components/tweetnacl/nacl-fast.min.js',
|
||||
'less!/customize/src/less2/pages/page-load.less',
|
||||
'css!/components/components-font-awesome/css/font-awesome.min.css',
|
||||
], function (Config, $, Netflux, h, Hash, Util, UI, NetConfig, nThen, Saferphore) {
|
||||
const wsUrl = NetConfig.getWebsocketURL();
|
||||
const nacl = window.nacl;
|
||||
let makeNetwork = function (cb) {
|
||||
Netflux.connect(wsUrl).then(function (network) {
|
||||
cb(null, network);
|
||||
}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
};
|
||||
|
||||
let Env = {
|
||||
users: {},
|
||||
channels: {},
|
||||
queries: 0,
|
||||
lag: [],
|
||||
errors: 0
|
||||
};
|
||||
let startSendDataEvt = Util.mkEvent(true);
|
||||
|
||||
let hk;
|
||||
|
||||
let edPublic = "gH12mjdXc1hGsVMtCJeoGTkBQRA21V0VOEGphoddmPM=";
|
||||
let edPrivate = "5V0tO8q1wKr62KIJadYdXaXvgG8f6FQtS6XHYrHYLzGAfXaaN1dzWEaxUy0Il6gZOQFBEDbVXRU4QamGh12Y8w==";
|
||||
|
||||
let hash = "/2/undefined/edit/UCrOzk5XEOP7qi"; // missing 10 characters
|
||||
|
||||
let makeHash = (id) => {
|
||||
let l = String(id).length;
|
||||
let add = 10 - l;
|
||||
let str = String(id);
|
||||
for(let i=0; i<add; i++) {
|
||||
str = 'x' + str;
|
||||
}
|
||||
let _hash = hash + str + '/';
|
||||
return _hash;
|
||||
};
|
||||
|
||||
let getMsg = isCp => {
|
||||
let base = nacl.util.encodeBase64(nacl.randomBytes(30));
|
||||
let repeat = isCp ? 300 : 5;
|
||||
let str = base;
|
||||
for (let i = 0; i < repeat; i++) {
|
||||
str += base;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
let signMsg = (isCp, secret) => {
|
||||
let msg = getMsg(isCp);
|
||||
let signKey = nacl.util.decodeBase64(secret.keys.signKey);
|
||||
let signed = nacl.util.encodeBase64(nacl.sign(nacl.util.decodeUTF8(msg), signKey));
|
||||
if (!isCp) { return signed; }
|
||||
let id = msg.slice(0,8);
|
||||
return `cp|${id}|${signed}`;
|
||||
};
|
||||
let makeData = function (id, cb) {
|
||||
let user = Env.users[id];
|
||||
if (!user || !user.wc || !user.secret || !user.isEmpty) {
|
||||
return void setTimeout(cb);
|
||||
}
|
||||
let n = nThen;
|
||||
for (let i = 1; i<=130; i++) {
|
||||
n = n(w => {
|
||||
let m = signMsg(!(i%50), user.secret);
|
||||
user.wc.bcast(m).then(w());
|
||||
}).nThen;
|
||||
}
|
||||
n(() => {
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
let clearDataCmd = max => {
|
||||
let cmd = 'Run the following commands to clear all the data\n';
|
||||
cmd += 'rm ';
|
||||
for (let i=0; i<max; i++) {
|
||||
let hash = makeHash(i);
|
||||
let secret = Hash.getSecrets('pad', hash);
|
||||
let chan = secret.channel;
|
||||
cmd += `${chan.slice(0,2)}/${chan}* `;
|
||||
}
|
||||
console.error(cmd);
|
||||
};
|
||||
|
||||
|
||||
let joinChan = (user, id, cb) => {
|
||||
if (!user || !user.network) { return; }
|
||||
let network = user.network;
|
||||
let hash = makeHash(id);
|
||||
let secret = Hash.getSecrets('pad', hash);
|
||||
if (!user.hash && !user.secret) {
|
||||
user.hash = hash;
|
||||
user.secret = secret;
|
||||
}
|
||||
user.isEmpty = true; // Only used with the makeData button
|
||||
let chan = Env.channels[secret.channel] = Env.channels[secret.channel] || {
|
||||
secret: secret,
|
||||
};
|
||||
let n = 0;
|
||||
network.on('message', (msg, sender) => {
|
||||
if (sender !== hk) { return; }
|
||||
let parsed = JSON.parse(msg);
|
||||
if (parsed.state === 1 && parsed.channel === secret.channel) {
|
||||
chan.total = n; // %50 to know if we should make a cp
|
||||
return void cb();
|
||||
}
|
||||
let m = parsed[4];
|
||||
if (parsed[3] !== secret.channel) { return; }
|
||||
if (!m) { return; }
|
||||
n++;
|
||||
user.isEmpty = false;
|
||||
});
|
||||
network.join(secret.channel).then(wc => {
|
||||
user.wc = wc;
|
||||
if (!hk) {
|
||||
wc.members.forEach(function (p) {
|
||||
if (p.length === 16) { hk = p; }
|
||||
});
|
||||
}
|
||||
let cfg = {
|
||||
metadata: {
|
||||
validateKey: secret.keys.validateKey,
|
||||
owners: [edPublic]
|
||||
}
|
||||
};
|
||||
let msg = ['GET_HISTORY', wc.id, cfg];
|
||||
network.sendto(hk, JSON.stringify(msg));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// TODO
|
||||
// Connect many websockets and have them run tasks
|
||||
// * [x] JOIN with 10 users per pad
|
||||
// * [x] GET_HISTORY
|
||||
// * [x] SEND content at random intervals
|
||||
// * UPLOAD random blobs
|
||||
// * RPC commands?
|
||||
|
||||
let setRandomInterval = f => {
|
||||
let delay = (Env.delay - 300)*2;
|
||||
let rdm = 300 + Math.floor(delay * Math.random());
|
||||
if (Env.stopPatch) { return; }
|
||||
setTimeout(function () {
|
||||
f();
|
||||
setRandomInterval(f);
|
||||
}, rdm);
|
||||
};
|
||||
|
||||
let startOneUser = function (i, init, cb) {
|
||||
let network;
|
||||
let myPads = [i];
|
||||
let me;
|
||||
nThen(w => {
|
||||
makeNetwork(w((err, _network) => {
|
||||
if (err) {
|
||||
w.abort();
|
||||
return void console.error(err);
|
||||
}
|
||||
network = _network;
|
||||
me = Env.users[i] = {
|
||||
network: network,
|
||||
myPads
|
||||
};
|
||||
}));
|
||||
}).nThen(w => {
|
||||
joinChan(me, i, w());
|
||||
}).nThen(w => {
|
||||
if (!init) { return; }
|
||||
makeData(i, w());
|
||||
}).nThen(w => {
|
||||
if (init) { return; }
|
||||
console.warn(i, me.secret.channel);
|
||||
let min = Math.max(Env.offset, i-5); // XXX 5 users per pad
|
||||
for (let j = min; j<i; j++) {
|
||||
myPads.push(j);
|
||||
joinChan(me, j, w());
|
||||
}
|
||||
}).nThen(w => {
|
||||
if (init) { return; }
|
||||
myPads.forEach(function (id) {
|
||||
let channel = (Env.users[id] && Env.users[id].secret) ? Env.users[id].secret.channel : null;
|
||||
if (channel==null) {
|
||||
console.log("Channel " + id + " is null");
|
||||
} else {
|
||||
let wc = me.network.webChannels.find(obj => {
|
||||
return obj.id === channel;
|
||||
});
|
||||
let chanObj = Env.channels[channel] || {};
|
||||
// Only fill the chan if it is not originally empty
|
||||
if (Env.users[id].isEmpty) { return; }
|
||||
startSendDataEvt.reg(function () {
|
||||
setRandomInterval(function () {
|
||||
let i = chanObj.total || 0;
|
||||
let m = signMsg(!(i%50), chanObj.secret);
|
||||
console.log('Send patch', channel, i%50);
|
||||
chanObj.total = i+1;
|
||||
Env.incQueries();
|
||||
let t = +new Date();
|
||||
wc.bcast(m).then(() => {
|
||||
let now = +new Date();
|
||||
Env.lag.push((now - t));
|
||||
}, err => {
|
||||
Env.errors++;
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}).nThen(w => {
|
||||
// TODO
|
||||
// RPC commands? Upload blob?
|
||||
}).nThen(w => {
|
||||
cb();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
let start = function (cb) {
|
||||
clearDataCmd(Env.numberUsers);
|
||||
var sem = Saferphore.create(20);
|
||||
let max = Env.numberUsers + Env.offset;
|
||||
nThen(w => {
|
||||
for (let i=Env.offset; i<max; i++) {
|
||||
let done = w();
|
||||
sem.take(function(give) {
|
||||
console.log('loading user ', i);
|
||||
startOneUser(i, false, () => {
|
||||
setTimeout(give(() => {
|
||||
done();
|
||||
}));
|
||||
console.log('loaded user ', i);
|
||||
});
|
||||
});
|
||||
}
|
||||
}).nThen(() => {
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
let makeAllData = function (cb) {
|
||||
var sem = Saferphore.create(10);
|
||||
let max = Env.numberUsers + Env.offset;
|
||||
nThen(w => {
|
||||
for (let i=Env.offset; i<max; i++) {
|
||||
let done = w();
|
||||
sem.take(function(give) {
|
||||
console.log('loading user ', i);
|
||||
startOneUser(i, true, () => {
|
||||
setTimeout(give(() => {
|
||||
done();
|
||||
}));
|
||||
console.log('loaded user ', i);
|
||||
});
|
||||
});
|
||||
}
|
||||
}).nThen(() => {
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
$(function () {
|
||||
let input = h('input', {type:'number',value:100,min:1, step:1});
|
||||
let label = h('label', [
|
||||
h('span', 'Number of users'),
|
||||
input
|
||||
]);
|
||||
|
||||
let inputOff = h('input', {type:'number',value:0,min:0, step:1});
|
||||
let labelOff = h('label', [
|
||||
h('span', 'User offset'),
|
||||
inputOff
|
||||
]);
|
||||
|
||||
let inputFreq = h('input', {type:'number',value:800,min:300, step:1});
|
||||
let labelFreq = h('label', [
|
||||
h('span', 'Average time between patches (ms) per user per channel'),
|
||||
inputFreq
|
||||
]);
|
||||
|
||||
let inputMax = h('input', {type:'number',value:0,min:0, step:1});
|
||||
let labelMax = h('label', [
|
||||
h('span', 'Max queries (0 for infinite)'),
|
||||
inputMax
|
||||
]);
|
||||
|
||||
let queries = h('span');
|
||||
let freq = h('span');
|
||||
let freqr = h('span');
|
||||
let time = h('span');
|
||||
let lag = h('span');
|
||||
let errors = h('span');
|
||||
let res = h('div', [
|
||||
queries,
|
||||
h('br'),
|
||||
time,
|
||||
h('br'),
|
||||
freq,
|
||||
h('br'),
|
||||
freqr,
|
||||
h('br'),
|
||||
lag,
|
||||
h('br'),
|
||||
errors
|
||||
]);
|
||||
|
||||
let button = h('button.btn.btn-primary', 'Start load testing');
|
||||
let buttonPatch = h('button.btn.btn-primary', {style:'display:none;'}, 'Start sending patches');
|
||||
let buttonStopPatch = h('button.btn.btn-danger-alt', {style:'display:none;'}, 'STOP sending patches');
|
||||
let buttonData = h('button.btn', 'Create data');
|
||||
var spinner = UI.makeSpinner();
|
||||
let content = h('div', [
|
||||
h('div.form', [
|
||||
label,
|
||||
labelOff,
|
||||
labelFreq,
|
||||
labelMax,
|
||||
h('nav', [button, buttonPatch, buttonStopPatch, buttonData, spinner.spinner]),
|
||||
res
|
||||
])
|
||||
]);
|
||||
|
||||
Env.incQueries = () => {
|
||||
Env.queries++;
|
||||
if (Env.maxQ && Env.queries >= Env.maxQ) {
|
||||
Env.stopPatch = true;
|
||||
$(buttonStopPatch).click();
|
||||
}
|
||||
};
|
||||
|
||||
let started = false;
|
||||
$(button).click(() => {
|
||||
if (started) { return; }
|
||||
spinner.spin();
|
||||
started = true;
|
||||
//$(button).remove();
|
||||
let users = Env.numberUsers = Number($(input).val());
|
||||
Env.offset = Number($(inputOff).val()) || 0;
|
||||
Env.delay = Number($(inputFreq).val()) || 800;
|
||||
Env.maxQ = Number($(inputMax).val()) || 0;
|
||||
if (typeof(users) !== "number" || !users) {
|
||||
return void console.error('Not a valid number');
|
||||
}
|
||||
$(buttonData).remove();
|
||||
start(() => {
|
||||
spinner.done();
|
||||
started = false;
|
||||
UI.log('READY: you can now start sending patches');
|
||||
$(buttonPatch).show();
|
||||
});
|
||||
});
|
||||
let qIt, fIt;
|
||||
let last = {};
|
||||
$(buttonPatch).click(() => {
|
||||
startSendDataEvt.fire();
|
||||
$(buttonPatch).remove();
|
||||
$(buttonStopPatch).show();
|
||||
Env.start = +new Date();
|
||||
last.t = +new Date();
|
||||
last.q = 0;
|
||||
qIt = setInterval(() => {
|
||||
$(queries).text('Queries: '+Env.queries);
|
||||
let q = Env.queries;
|
||||
let now = +new Date();
|
||||
let diffTime = (now-Env.start)/1000;
|
||||
let f = Math.floor(q/diffTime);
|
||||
const average = Math.round((Env.lag.length && Env.lag.reduce((a, b) => a + b, 0) / Env.lag.length)) || 0;
|
||||
|
||||
$(freq).text('Queries/s (all): '+f);
|
||||
$(time).text('Time: '+Math.floor(diffTime)+'s');
|
||||
$(lag).text('Avg response time: '+average+'ms');
|
||||
$(errors).text('Errors: '+Env.errors);
|
||||
Env.lag = [];
|
||||
}, 200);
|
||||
fIt = setInterval(() => {
|
||||
let q = Env.queries;
|
||||
let now = +new Date();
|
||||
let fr = Math.floor(1000*(Env.queries-last.q)/(now-last.t));
|
||||
|
||||
last.t = +new Date();
|
||||
last.q = q;
|
||||
$(freqr).text('Queries/s (recent): '+fr);
|
||||
}, 1000);
|
||||
});
|
||||
$(buttonStopPatch).click(() => {
|
||||
Env.stopPatch = true;
|
||||
clearInterval(qIt);
|
||||
clearInterval(fIt);
|
||||
$(buttonStopPatch).remove();
|
||||
});
|
||||
let startedData = false;
|
||||
$(buttonData).click(() => {
|
||||
if (startedData) { return; }
|
||||
startedData = true;
|
||||
spinner.spin();
|
||||
let users = Env.numberUsers = Number($(input).val());
|
||||
Env.offset = Number($(inputOff).val()) || 0;
|
||||
Env.delay = Number($(inputFreq).val()) || 800;
|
||||
Env.maxQ = Number($(inputMax).val()) || 0;
|
||||
if (typeof(users) !== "number" || !users) {
|
||||
return void console.error('Not a valid number');
|
||||
}
|
||||
$(button).remove();
|
||||
$(buttonData).remove();
|
||||
makeAllData(() => {
|
||||
spinner.done();
|
||||
UI.log('DONE');
|
||||
});
|
||||
});
|
||||
$('body').append(content);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue