Commit 705349cf authored by Benjamin Fischer's avatar Benjamin Fischer

[test/bench] finalized testing/benchmarking script

parent 2c3ded56
......@@ -14,11 +14,17 @@ const rdx = (req, { qs = {}, ...opts } = {}) => {
return ret;
};
const sleep = delay => new Promise(done => setTimeout(done, delay));
const dirOp = (op, a, b) => {
const b2h = (b, { t = 800, s = 1024, u = "B" } = {}) => {
for (let i of " kMGTE")
if (b < t) return b.toPrecision(3) + i + u;
else b /= s;
};
const dirOp = op => (a, b) => {
const c = {};
for (let key of a) c[key] = op(a[key], b[key], key);
for (let key in a) c[key] = op(a[key], b[key], key);
return c;
}
};
const dirSum = dirOp((a, b) => (a || 0) + (b || 0));
const pageFiles = {};
for (const key of ["login", "index", "editor"])
......@@ -84,7 +90,7 @@ class CodeEditor extends View {
await new Promise(done => this.once("done", done));
}
async run({ payload, iter = 0, output, delay = 20e3, cmd }) {
async run({ payload, iter = 0, output, delay, cmd }) {
if (output) output = `${dirname(this.path)}/${output}`;
await this.list();
if (payload) await this.save(payload);
......@@ -96,52 +102,80 @@ class CodeEditor extends View {
await this.user.thumb(output);
await this.user.load(output);
}
await sleep(delay);
await sleep(delay());
}
}
}
class Stats {
fill(other) {
for (let key in other) this.count(key, other[key] || 0);
}
count(key, num = 1) {
this[key] = (this[key] || 0) + (num || 0);
}
}
class User extends EventEmitter {
constructor(username, { agentOptions, baseUrl } = {}) {
constructor(username, { agentOptions, baseUrl, stats = new Stats() } = {}) {
super();
this.stats = stats;
this.req = rdx(request, {
baseUrl,
jar: request.jar(),
time: true,
transform: (body, res) => {
this.reqs++;
this.recv += body ? body.length : 0;
stats.fill({
reqs: 1,
recv: body ? body.length : 0
});
if (!res.request.uri.href.includes("poll"))
stats.fill(res.timingPhases);
let status = res.statusCode;
if ((res.headers["content-type"] || "").includes("json")) {
const json = JSON.parse(body);
status = json.code;
body = json.data;
}
if (status != 200)
console.error(
"ERR[%d] URL=%s req.body=%o len(res)=%d res=%o",
this.srv =
(res.headers["x-app-server"] || "").replace(/.+\//, "") || this.srv;
const lg = msg => [
"%s[%d@%s/%s] URL=%s time=%d req.body=%o len(res)=%d res=%o",
msg,
status,
this.srv,
this.hn,
res.request.uri.href,
res.timingPhases.total,
res.request.body,
res.body && res.body.length,
res.body
res.headers["content-type"].includes("text") ? res.body : null
];
if (status != 200) {
console.error(...lg("ERR"));
throw new Error(
`failed[${status}@${this.srv}/${this.hn}] ${lg().slice(2)}`
);
// console.error("failed: URL=%s body=%o", res.request.uri.href, res.request.body);
// throw new Error(
// `failed: URL=${res.request.uri.href} body=${res.request.body}`
// );
} else if (res.timingPhases.total > 25e3) console.log(...lg("SLO"));
// else console.log(...lg("RES"));
return body;
},
agentOptions: {
maxSockets: 6,
keepAlive: true,
keepAliveMsecs: 10,
maxFreeSockets: 1,
...agentOptions
}
},
pool: {}
});
this.username = username;
this.bust = `${username}-${rando(16)}`;
this.polls = 0;
this.reqs = 0;
this.recv = 0;
this.srv = "";
this.hn = "";
this.on("data", ({ topic, data: { workspaceId } = {} }) => {
// console.log("data: %o", data);
if (topic.startsWith("workspace.")) this.emit(`${topic}:${workspaceId}`);
});
}
async static(key) {
await Promise.all(
......@@ -149,42 +183,54 @@ class User extends EventEmitter {
);
}
async login(username, password) {
async login(password, quick) {
if (!quick) {
await this.req.get("/login");
await this.static("login");
await this.req.post("/ajax/login", { form: { username, password } });
this.req.qs._windowId = rando(36);
}
get poll() {
return !!this._pollStop;
}
set poll(val) {
val = !!val;
if (val == this.poll) return;
if (val) this._poll();
else {
const stop = this._pollStop;
delete this._pollStop;
stop();
}
await this.req.post("/ajax/login", {
form: { username: this.username, password }
});
this.req.qs._windowId = rando(36);
}
async _poll() {
poll() {
let active = true;
this._pollStop = () => {
active = false;
};
const prom = (async () => {
while (active) {
const res = await this.req.get("/bus/poll");
this.polls++;
this.stats.count("poll");
if (res) for (const data of res) this.emit("data", JSON.parse(data));
}
})();
this.pollStop = prom.stop = async () => {
active = false;
await prom;
delete this.poll;
delete this.pollStop;
};
this.poll = () => prom;
return prom;
}
pollStop() {}
async workspace(wid, password) {
await this.req.post("/ajax/connectworkspace", { form: { wid, password } });
const wsInfo = (await this.req.get("/ajax/getworkspacedata"))[wid];
if (!wsInfo) throw new Error(`no such workspace: ${wid}`);
if (wsInfo.connection_status !== "connected") {
await this.req.post("/ajax/connectworkspace", {
form: { wid, password }
});
this.poll();
await new Promise(done => this.once(`workspace.connected:${wid}`, done));
}
this.req.qs._workspaceId = wid;
this.hn = (await this.load("/etc/hostname")).trim();
process.stdout.write(
`connected: ${this.username.padEnd(17)} ${this.srv.padEnd(
8
)} ${this.hn.padEnd(11)}`.padEnd(79) + "\n"
);
}
async thumb(path, height = 100, width = height) {
......@@ -194,7 +240,7 @@ class User extends EventEmitter {
}
async load(path) {
await this.req.get("/fs/getfile", {
return await this.req.get("/fs/getfile", {
qs: { path, _mtime: Date.now() }
});
}
......@@ -204,35 +250,155 @@ class User extends EventEmitter {
}
async logout() {
this.poll = false;
const wid = this.req.qs._workspaceId;
if (wid !== undefined) {
await this.req.post("/ajax/disconnectworkspace", {
form: { wid }
});
delete this.req.qs._workspaceId;
}
await this.pollStop();
await this.req.get("logout");
delete this.req.qs._windowId;
}
}
// actual test
const payload = readFileSync("payload.py");
async runEdit(password, { wsId, path, ...opts }) {
await this.login(password);
await this.req.get("");
await this.static("index");
// this.poll();
await this.workspace(wsId, password);
await this.editor(path).run(opts);
await this.logout();
return this;
}
async runStorm(password, { wsId, ...opts } = {}) {
await this.login(password, true);
await this.workspace(wsId, password);
await this.pollStop();
await this.storm(opts);
await this.logout();
return this;
}
const run = async (username, password) => {
async storm({ iter = 1e4, conc = 5, delay } = {}) {
const edit = this.editor("$HOME/test.py");
const q = new Set();
for (let i = iter; i--; ) {
while (q.size < conc) {
const prom = edit.list();
prom.finally(() => q.delete(prom));
q.add(prom);
}
await Promise.race(Array.from(q));
if (delay) await sleep(delay());
}
await Promise.all(Array.from(q));
}
}
const loop = async ({
conc = 10,
delay = () => 10e3 * (1 + 2 * Math.random()),
total = 10000,
genUsername = () => "testuser",
password,
statusPassword,
wsId,
baseUrl,
storm,
edit
} = {}) => {
if (!password) throw new Error(`"password" is missing`);
if (!baseUrl) throw new Error(`"baseUrl" is missing`);
if (!wsId) throw new Error(`"wsId" is missing`);
if (!wsId) throw new Error(`"wsId" is missing`);
const users = new Set();
const stats = new Stats();
let bad = 0;
for (let stop, i = 0, d = 0; i < total || users.size; ) {
const username = genUsername(i);
if (
i < total &&
!stop &&
users.size < conc &&
d < Date.now() &&
!Array.from(users).some(user => user.username === username)
) {
d = Date.now() + delay();
i++;
const user = new User(username, {
baseUrl: "http://localhost:4282/test-server/"
// baseUrl: "https://vispa.physik.rwth-aachen.de/server/"
});
await user.login(username, password);
await user.req.get("");
// await user.static("index");
// user.on("data", data => console.log("data: %o", data));
user.poll = true;
const base = "$HOME/test";
await user.workspace("1", password);
await user.editor(`${base}/test.py`).run({
payload,
iter: 0,
delay: 2e3,
output: "test.png"
baseUrl,
stats
});
await user.logout();
return user;
const prom = user[storm ? "runStorm" : "runEdit"](password, {
wsId,
...(storm || edit)
})
.catch(async err => {
console.error("[%d] failed: %s", i, err);
bad++;
if (statusPassword) {
const status = await user.req.post("status", {
form: { password: statusPassword }
});
await new Promise((done, fail) =>
writeFile(`bad/${i}.html`, status, err =>
err ? fail(err) : done()
)
);
}
})
.finally(() => users.delete(user));
users.add(user);
}
process.stdout.write(
`users=${users.size}/${conc}@${i}/${total} polls=${stats.poll} reqs=${
stats.reqs
} recv=${b2h(stats.recv)} firstByte=${(
stats.firstByte / stats.reqs
).toFixed(0)}ms bad=${bad}`.padEnd(79) + "\r"
);
if (stop && !users.size) break;
await sleep(100);
switch (process.stdin.read(1) + "") {
case "n":
d = 0;
break;
case "+":
conc++;
break;
case "-":
conc && conc--;
break;
case "0":
conc = 0;
break;
case "q":
stop = true;
break;
}
}
process.stdout.write("\n");
process.stdin.pause();
};
run("testuser", "testuser").catch(err => console.error(err));
loop({
delay: () => 15 * (1 + Math.random()),
conc: 15,
total: 500,
// storm: { conc: 1, iter: 1 },
// storm: { conc: 5 },
edit: {
path: "$HOME/test.py",
payload: readFileSync("payload.py"),
iter: 50,
delay: () => 1e3 * (1 + 15 * Math.random()),
output: "test.png"
}
});
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment