Compare commits

...

22 commits
main ... load

Author SHA1 Message Date
yflory
02e0d3ba5d Merge branch 'sodium' into load 2024-07-05 14:31:43 +02:00
yflory
7b8e83d72b Merge branch 'main' into load 2024-07-05 14:30:24 +02:00
yflory
411039abeb Add support for a serverside crypto plugin 2024-06-27 16:26:57 +02:00
yflory
43cfc926bb Merge branch 'monitoring' into load 2024-06-24 11:22:38 +02:00
yflory
1d639a7653 update package.json and package-lock.json 2024-06-12 15:32:26 +02:00
yflory
1310979994 lint compliance 2024-06-12 15:01:57 +02:00
yflory
2de0a0d4b9 Merge branch 'stats' into monitoring 2024-06-12 14:59:55 +02:00
yflory
8c6acf9578 Guard against null channels 2024-05-23 17:41:26 +02:00
yflory
73891d819e Add new monitoring data 2024-05-23 16:01:38 +02:00
yflory
83dd6fa16c Add response time to load testing results 2024-05-23 11:57:46 +02:00
yflory
a8e03cef9d Fix offset with load test data creation 2024-05-22 18:16:12 +02:00
yflory
c17312c3fb More config options for load test 2024-05-22 16:28:37 +02:00
yflory
cf153b8474 Load testing script 2024-05-22 13:54:50 +02:00
yflory
eb1249b6cd Fix cpu usage monitoring 2024-05-17 11:22:32 +02:00
yflory
332edec162 Merge branch 'main' into monitoring 2024-05-17 10:45:37 +02:00
yflory
bde6ef8044 Add CPU monitoring 2024-05-17 10:30:39 +02:00
yflory
9b8e487b70 Merge branch 'main' into monitoring 2024-05-16 17:19:40 +02:00
yflory
99f1cf650e Fix package.json 2024-05-16 17:18:00 +02:00
yflory
9c22a25ba2 Merge branch 'main' into monitoring 2024-05-16 17:15:09 +02:00
yflory
fff4e7581e Add heap memory data to the monitoring tools 2023-08-30 16:13:09 +02:00
yflory
49af0533b5 lint compliance 2023-08-22 18:36:48 +02:00
yflory
4f2a48a72e Add memory monitoring tools 2023-08-22 18:09:22 +02:00
16 changed files with 835 additions and 6 deletions

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

View file

@ -108,7 +108,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)

24
lib/crypto.js Normal file
View 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);
});
};

View file

@ -252,6 +252,7 @@ module.exports.create = function (config) {
curvePublic: Nacl.util.encodeBase64(curve.publicKey),
selfDestructTo: {},
monitoring: {}
};
(function () {
@ -415,6 +416,7 @@ const BAD = [
'limits',
'customLimits',
'scheduleDecree',
'monitoring',
'httpServer',

View file

@ -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);
@ -219,6 +318,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';
@ -799,7 +905,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
View 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
};

View file

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

View file

@ -16,6 +16,8 @@ const Logger = require("../log");
const Tasks = require("../storage/tasks");
const Nacl = require('tweetnacl/nacl-fast');
const Eviction = require("../eviction");
const Monitoring = require('../monitoring');
const CPCrypto = require('../crypto');
const Env = {
Log: {},
@ -56,6 +58,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) {
@ -101,6 +110,10 @@ const init = function (config, _cb) {
}
Env.tasks = tasks;
}));
}).nThen(function (w) {
CPCrypto.init(w(function (err, crypto) {
Env.crypto = crypto;
}));
}).nThen(function () {
cb();
});
@ -680,7 +693,8 @@ COMMANDS.INLINE = function (data, cb) {
return void cb("E_BADKEY");
}
// 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) {
return void cb("FAILED");
}
@ -720,7 +734,8 @@ const checkDetachedSignature = function (signedMsg, signature, publicKey) {
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");
}
};

View file

@ -9,6 +9,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;
@ -162,6 +163,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) {
@ -226,7 +234,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) {
@ -263,9 +273,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) {
@ -281,6 +292,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) {

25
package-lock.json generated
View file

@ -47,6 +47,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",
@ -1202,6 +1203,11 @@
"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": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
@ -3931,6 +3937,17 @@
"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": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/prompt-actions/-/prompt-actions-3.0.2.tgz",
@ -5324,6 +5341,14 @@
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/terminal-paginator/-/terminal-paginator-2.0.2.tgz",

View file

@ -50,6 +50,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",

61
scripts/testcrypto.js Normal file
View 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');
});

View file

@ -15,6 +15,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();
@ -49,6 +50,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;
@ -93,6 +99,7 @@ nThen(function (w) {
var launchWorker = (online) => {
var worker = Cluster.fork(workerState);
var pid = worker.process.pid;
worker.on('online', () => {
online();
});
@ -122,6 +129,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', {
@ -163,6 +171,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);

View file

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