Compare commits
22 commits
Author | SHA1 | Date | |
---|---|---|---|
|
02e0d3ba5d | ||
|
7b8e83d72b | ||
|
411039abeb | ||
|
43cfc926bb | ||
|
1d639a7653 | ||
|
1310979994 | ||
|
2de0a0d4b9 | ||
|
8c6acf9578 | ||
|
73891d819e | ||
|
83dd6fa16c | ||
|
a8e03cef9d | ||
|
c17312c3fb | ||
|
cf153b8474 | ||
|
eb1249b6cd | ||
|
332edec162 | ||
|
bde6ef8044 | ||
|
9b8e487b70 | ||
|
99f1cf650e | ||
|
9c22a25ba2 | ||
|
fff4e7581e | ||
|
49af0533b5 | ||
|
4f2a48a72e |
16 changed files with 835 additions and 6 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ nThen(function (w) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// spawn ws server and attach netflux event handlers
|
// 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('channelClose', historyKeeper.channelClose)
|
||||||
.on('channelMessage', historyKeeper.channelMessage)
|
.on('channelMessage', historyKeeper.channelMessage)
|
||||||
.on('channelOpen', historyKeeper.channelOpen)
|
.on('channelOpen', historyKeeper.channelOpen)
|
||||||
|
|
24
lib/crypto.js
Normal file
24
lib/crypto.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
const Nacl = require('tweetnacl/nacl-fast');
|
||||||
|
const CPCrypto = module.exports;
|
||||||
|
const plugins = require('./plugin-manager');
|
||||||
|
|
||||||
|
CPCrypto.init = (cb) => {
|
||||||
|
const crypto = {};
|
||||||
|
crypto.open = (signedMsg, validateKey) => {
|
||||||
|
return Nacl.sign.open(signedMsg, validateKey);
|
||||||
|
};
|
||||||
|
crypto.detachedVerify = (signedBuffer, signatureBuffer, validateKey) => {
|
||||||
|
return Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer);
|
||||||
|
};
|
||||||
|
if (plugins.SODIUM && plugins.SODIUM.crypto) {
|
||||||
|
let c = plugins.SODIUM.crypto;
|
||||||
|
if (c.open) { crypto.open = c.open; }
|
||||||
|
if (c.detachedVerify) { crypto.detachedVerify = c.detachedVerify; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make async because we might need it later with libsodium's promise
|
||||||
|
// libsodium.ready.then(() => {});
|
||||||
|
setTimeout(() => {
|
||||||
|
cb(void 0, crypto);
|
||||||
|
});
|
||||||
|
};
|
|
@ -252,6 +252,7 @@ module.exports.create = function (config) {
|
||||||
curvePublic: Nacl.util.encodeBase64(curve.publicKey),
|
curvePublic: Nacl.util.encodeBase64(curve.publicKey),
|
||||||
|
|
||||||
selfDestructTo: {},
|
selfDestructTo: {},
|
||||||
|
monitoring: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
@ -415,6 +416,7 @@ const BAD = [
|
||||||
'limits',
|
'limits',
|
||||||
'customLimits',
|
'customLimits',
|
||||||
'scheduleDecree',
|
'scheduleDecree',
|
||||||
|
'monitoring',
|
||||||
|
|
||||||
'httpServer',
|
'httpServer',
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,9 @@ const BlobStore = require("./storage/blob");
|
||||||
const BlockStore = require("./storage/block");
|
const BlockStore = require("./storage/block");
|
||||||
const plugins = require("./plugin-manager");
|
const plugins = require("./plugin-manager");
|
||||||
|
|
||||||
|
const Prometheus = require('prom-client');
|
||||||
|
const Monitoring = require('./monitoring');
|
||||||
|
|
||||||
const DEFAULT_QUERY_TIMEOUT = 5000;
|
const DEFAULT_QUERY_TIMEOUT = 5000;
|
||||||
const PID = process.pid;
|
const PID = process.pid;
|
||||||
|
|
||||||
|
@ -66,6 +69,102 @@ Env.incrementBytesWritten = function () {};
|
||||||
|
|
||||||
const EVENTS = {};
|
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 */) {
|
EVENTS.ENV_UPDATE = function (data /*, cb */) {
|
||||||
try {
|
try {
|
||||||
Env = JSON.parse(data);
|
Env = JSON.parse(data);
|
||||||
|
@ -219,6 +318,13 @@ const wsProxy = createProxyMiddleware({
|
||||||
|
|
||||||
app.use('/cryptpad_websocket', wsProxy);
|
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) => {
|
app.use('/ssoauth', (req, res, next) => {
|
||||||
if (SSOUtils && req && req.body && req.body.SAMLResponse) {
|
if (SSOUtils && req && req.body && req.body.SAMLResponse) {
|
||||||
req.method = 'GET';
|
req.method = 'GET';
|
||||||
|
@ -799,7 +905,14 @@ nThen(function (w) {
|
||||||
}));
|
}));
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
// TODO inform the parent process that this worker is ready
|
// 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) {
|
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];
|
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') {
|
if (command === 'UPLOAD') {
|
||||||
// UPLOAD is a special case that skips signature validation
|
// UPLOAD is a special case that skips signature validation
|
||||||
// intentional fallthrough behaviour
|
// intentional fallthrough behaviour
|
||||||
|
|
|
@ -16,6 +16,8 @@ const Logger = require("../log");
|
||||||
const Tasks = require("../storage/tasks");
|
const Tasks = require("../storage/tasks");
|
||||||
const Nacl = require('tweetnacl/nacl-fast');
|
const Nacl = require('tweetnacl/nacl-fast');
|
||||||
const Eviction = require("../eviction");
|
const Eviction = require("../eviction");
|
||||||
|
const Monitoring = require('../monitoring');
|
||||||
|
const CPCrypto = require('../crypto');
|
||||||
|
|
||||||
const Env = {
|
const Env = {
|
||||||
Log: {},
|
Log: {},
|
||||||
|
@ -56,6 +58,13 @@ const init = function (config, _cb) {
|
||||||
Env.archiveRetentionTime = config.archiveRetentionTime;
|
Env.archiveRetentionTime = config.archiveRetentionTime;
|
||||||
Env.accountRetentionTime = config.accountRetentionTime;
|
Env.accountRetentionTime = config.accountRetentionTime;
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
process.send({
|
||||||
|
monitoring: true,
|
||||||
|
data: Monitoring.getData('db-worker')
|
||||||
|
});
|
||||||
|
}, Monitoring.interval);
|
||||||
|
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
Store.create(config, w(function (err, _store) {
|
Store.create(config, w(function (err, _store) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -101,6 +110,10 @@ const init = function (config, _cb) {
|
||||||
}
|
}
|
||||||
Env.tasks = tasks;
|
Env.tasks = tasks;
|
||||||
}));
|
}));
|
||||||
|
}).nThen(function (w) {
|
||||||
|
CPCrypto.init(w(function (err, crypto) {
|
||||||
|
Env.crypto = crypto;
|
||||||
|
}));
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
|
@ -680,7 +693,8 @@ COMMANDS.INLINE = function (data, cb) {
|
||||||
return void cb("E_BADKEY");
|
return void cb("E_BADKEY");
|
||||||
}
|
}
|
||||||
// validate the message
|
// validate the message
|
||||||
const validated = Nacl.sign.open(signedMsg, validateKey);
|
//const validated = Nacl.sign.open(signedMsg, validateKey);
|
||||||
|
const validated = Env.crypto.open(signedMsg, validateKey);
|
||||||
if (!validated) {
|
if (!validated) {
|
||||||
return void cb("FAILED");
|
return void cb("FAILED");
|
||||||
}
|
}
|
||||||
|
@ -720,7 +734,8 @@ const checkDetachedSignature = function (signedMsg, signature, publicKey) {
|
||||||
throw new Error("INVALID_SIGNATURE_LENGTH");
|
throw new Error("INVALID_SIGNATURE_LENGTH");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
|
//if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
|
||||||
|
if (Env.crypto.detachedVerify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
|
||||||
throw new Error("FAILED");
|
throw new Error("FAILED");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ const { fork } = require('child_process');
|
||||||
const Workers = module.exports;
|
const Workers = module.exports;
|
||||||
const PID = process.pid;
|
const PID = process.pid;
|
||||||
const Block = require("../storage/block");
|
const Block = require("../storage/block");
|
||||||
|
const Monitoring = require('../monitoring');
|
||||||
|
|
||||||
const DB_PATH = 'lib/workers/db-worker';
|
const DB_PATH = 'lib/workers/db-worker';
|
||||||
const MAX_JOBS = 16;
|
const MAX_JOBS = 16;
|
||||||
|
@ -162,6 +163,13 @@ Workers.initialize = function (Env, config, _cb) {
|
||||||
if (res.log) {
|
if (res.log) {
|
||||||
return void handleLog(res.log, res.label, res.info);
|
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
|
// but don't bother handling things addressed to other processes
|
||||||
// since it's basically guaranteed not to work
|
// since it's basically guaranteed not to work
|
||||||
if (res.pid !== PID) {
|
if (res.pid !== PID) {
|
||||||
|
@ -226,7 +234,9 @@ Workers.initialize = function (Env, config, _cb) {
|
||||||
handleResponse(state, res);
|
handleResponse(state, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let pid = worker.pid;
|
||||||
var substituteWorker = Util.once(function () {
|
var substituteWorker = Util.once(function () {
|
||||||
|
Monitoring.remove(Env, pid);
|
||||||
Env.Log.info("SUBSTITUTE_DB_WORKER", '');
|
Env.Log.info("SUBSTITUTE_DB_WORKER", '');
|
||||||
var idx = workers.indexOf(state);
|
var idx = workers.indexOf(state);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
|
@ -263,9 +273,10 @@ Workers.initialize = function (Env, config, _cb) {
|
||||||
};
|
};
|
||||||
|
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
var limit = Env.maxWorkers;
|
var limit = Env.maxWorkers || OS.cpus().length;
|
||||||
var logged;
|
var logged;
|
||||||
|
|
||||||
|
/*
|
||||||
OS.cpus().forEach(function (cpu, index) {
|
OS.cpus().forEach(function (cpu, index) {
|
||||||
if (limit && index >= limit) {
|
if (limit && index >= limit) {
|
||||||
if (!logged) {
|
if (!logged) {
|
||||||
|
@ -281,6 +292,14 @@ Workers.initialize = function (Env, config, _cb) {
|
||||||
return void cb(err);
|
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 () {
|
}).nThen(function () {
|
||||||
Env.computeIndex = function (Env, channel, cb) {
|
Env.computeIndex = function (Env, channel, cb) {
|
||||||
Env.store.getWeakLock(channel, function (next) {
|
Env.store.getWeakLock(channel, function (next) {
|
||||||
|
|
25
package-lock.json
generated
25
package-lock.json
generated
|
@ -47,6 +47,7 @@
|
||||||
"open-sans-fontface": "^1.4.0",
|
"open-sans-fontface": "^1.4.0",
|
||||||
"openid-client": "^5.4.2",
|
"openid-client": "^5.4.2",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
|
"prom-client": "^14.2.0",
|
||||||
"prompt-confirm": "^2.0.4",
|
"prompt-confirm": "^2.0.4",
|
||||||
"pull-stream": "^3.6.1",
|
"pull-stream": "^3.6.1",
|
||||||
"require-css": "0.1.10",
|
"require-css": "0.1.10",
|
||||||
|
@ -1202,6 +1203,11 @@
|
||||||
"node": ">= 0.6.0"
|
"node": ">= 0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bintrees": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||||
|
@ -3931,6 +3937,17 @@
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prom-client": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tdigest": "^0.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prompt-actions": {
|
"node_modules/prompt-actions": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/prompt-actions/-/prompt-actions-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/prompt-actions/-/prompt-actions-3.0.2.tgz",
|
||||||
|
@ -5324,6 +5341,14 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tdigest": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
|
||||||
|
"dependencies": {
|
||||||
|
"bintrees": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/terminal-paginator": {
|
"node_modules/terminal-paginator": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/terminal-paginator/-/terminal-paginator-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/terminal-paginator/-/terminal-paginator-2.0.2.tgz",
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"open-sans-fontface": "^1.4.0",
|
"open-sans-fontface": "^1.4.0",
|
||||||
"openid-client": "^5.4.2",
|
"openid-client": "^5.4.2",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
|
"prom-client": "^14.2.0",
|
||||||
"prompt-confirm": "^2.0.4",
|
"prompt-confirm": "^2.0.4",
|
||||||
"pull-stream": "^3.6.1",
|
"pull-stream": "^3.6.1",
|
||||||
"require-css": "0.1.10",
|
"require-css": "0.1.10",
|
||||||
|
|
61
scripts/testcrypto.js
Normal file
61
scripts/testcrypto.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
let SodiumNative = require('sodium-native');
|
||||||
|
let Nacl = require('tweetnacl/nacl-fast');
|
||||||
|
let LibSodium = require('libsodium-wrappers');
|
||||||
|
|
||||||
|
|
||||||
|
let msgStr = "This is a test";
|
||||||
|
let keys = Nacl.sign.keyPair();
|
||||||
|
let pub = keys.publicKey;
|
||||||
|
|
||||||
|
let msg = Nacl.util.decodeUTF8(msgStr);
|
||||||
|
let signedMsg = Nacl.sign(msg, keys.secretKey);
|
||||||
|
let sig = signedMsg.subarray(0, 64);
|
||||||
|
|
||||||
|
LibSodium.ready.then(() => {
|
||||||
|
|
||||||
|
/*
|
||||||
|
console.log('tweetnacl open');
|
||||||
|
console.log(!!Nacl.sign.open(signedMsg, pub));
|
||||||
|
console.log('tweetnacl detached');
|
||||||
|
console.log(Nacl.sign.detached.verify(msg, sig, pub));
|
||||||
|
console.log('sodium-native open');
|
||||||
|
console.log(SodiumNative.crypto_sign_open(msg, signedMsg, pub));
|
||||||
|
console.log('sodium-native detached');
|
||||||
|
console.log(SodiumNative.crypto_sign_verify_detached(sig, msg, pub));
|
||||||
|
LibSodium.ready.then(() => {
|
||||||
|
console.log('libsodium open');
|
||||||
|
console.log(!!LibSodium.crypto_sign_open(signedMsg, pub));
|
||||||
|
console.log('libsodium detached');
|
||||||
|
console.log(LibSodium.crypto_sign_verify_detached(sig, msg, pub));
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
const n = 10000;
|
||||||
|
let a;
|
||||||
|
|
||||||
|
console.log('start sodium-native');
|
||||||
|
a = +new Date();
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
SodiumNative.crypto_sign_open(msg, signedMsg, pub);
|
||||||
|
SodiumNative.crypto_sign_verify_detached(sig, msg, pub);
|
||||||
|
}
|
||||||
|
console.log('end sodium-native ', (+new Date() - a), ' ms');
|
||||||
|
|
||||||
|
console.log('start libsodium');
|
||||||
|
a = +new Date();
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
LibSodium.crypto_sign_open(signedMsg, pub);
|
||||||
|
LibSodium.crypto_sign_verify_detached(sig, msg, pub);
|
||||||
|
}
|
||||||
|
console.log('end libsodium ', (+new Date() - a), ' ms');
|
||||||
|
|
||||||
|
console.log('start tweetnacl');
|
||||||
|
a = +new Date();
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
Nacl.sign.open(signedMsg, pub);
|
||||||
|
Nacl.sign.detached.verify(msg, sig, pub);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('end tweetnacl ', (+new Date() - a), ' ms');
|
||||||
|
});
|
||||||
|
|
21
server.js
21
server.js
|
@ -15,6 +15,7 @@ var config = require("./lib/load-config");
|
||||||
var Environment = require("./lib/env");
|
var Environment = require("./lib/env");
|
||||||
var Env = Environment.create(config);
|
var Env = Environment.create(config);
|
||||||
var Default = require("./lib/defaults");
|
var Default = require("./lib/defaults");
|
||||||
|
var Monitoring = require('./lib/monitoring');
|
||||||
|
|
||||||
var app = Express();
|
var app = Express();
|
||||||
|
|
||||||
|
@ -49,6 +50,11 @@ COMMANDS.GET_PROFILING_DATA = function (msg, cb) {
|
||||||
cb(void 0, Env.bytesWritten);
|
cb(void 0, Env.bytesWritten);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
COMMANDS.MONITORING = function (msg, cb) {
|
||||||
|
Monitoring.applyToEnv(Env, msg.data);
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
require("./lib/log").create(config, w(function (_log) {
|
require("./lib/log").create(config, w(function (_log) {
|
||||||
Env.Log = _log;
|
Env.Log = _log;
|
||||||
|
@ -93,6 +99,7 @@ nThen(function (w) {
|
||||||
|
|
||||||
var launchWorker = (online) => {
|
var launchWorker = (online) => {
|
||||||
var worker = Cluster.fork(workerState);
|
var worker = Cluster.fork(workerState);
|
||||||
|
var pid = worker.process.pid;
|
||||||
worker.on('online', () => {
|
worker.on('online', () => {
|
||||||
online();
|
online();
|
||||||
});
|
});
|
||||||
|
@ -122,6 +129,7 @@ nThen(function (w) {
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('exit', (code, signal) => {
|
worker.on('exit', (code, signal) => {
|
||||||
|
Monitoring.remove(Env, pid);
|
||||||
if (!signal && code === 0) { return; }
|
if (!signal && code === 0) { return; }
|
||||||
// relaunch http workers if they crash
|
// relaunch http workers if they crash
|
||||||
Env.Log.error('HTTP_WORKER_EXIT', {
|
Env.Log.error('HTTP_WORKER_EXIT', {
|
||||||
|
@ -163,6 +171,19 @@ nThen(function (w) {
|
||||||
broadcast('FLUSH_CACHE', Env.FRESH_KEY);
|
broadcast('FLUSH_CACHE', Env.FRESH_KEY);
|
||||||
}, 250);
|
}, 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.envUpdated.reg(throttledEnvChange);
|
||||||
Env.cacheFlushed.reg(throttledCacheFlush);
|
Env.cacheFlushed.reg(throttledCacheFlush);
|
||||||
|
|
||||||
|
|
|
@ -789,7 +789,8 @@ define([
|
||||||
|
|
||||||
ownedPads.forEach(function (c) {
|
ownedPads.forEach(function (c) {
|
||||||
var w = waitFor();
|
var w = waitFor();
|
||||||
sem.take(function (give) {
|
sem.take(function (_give) {
|
||||||
|
var give = _give();
|
||||||
var otherOwners = false;
|
var otherOwners = false;
|
||||||
nThen(function (_w) {
|
nThen(function (_w) {
|
||||||
// Don't check server metadata for blobs
|
// 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