mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
fix(instances): pages poll one request at a time, and not while hidden
The VM page polls the VM state every 5 s and, while its Stats tab is open, the statistics every 10 s; the compute overview polls the host graph every 5 s. All used setInterval: - A new request went out on schedule while the previous one was still open. When a host goes silent, a stats request waits up to 30-45 s, so each open page held several sync workers at once. - Each time the Stats tab was shown, the page made new charts and a new interval without stopping the old ones, and polling went on on the other tabs of the page. - Hidden pages kept polling at full rate. Measured in a browser against a test server (old code / new): Stats tab shown 4 times, 31 s on it: 12 / 2 requests; 31 s on another tab of the page: 12 / 0; page hidden for 31 s: 15 / 0, and the new code asks once right after the page is shown again; with 25 s answers, at most 15 / 1 requests open at once. static/js/poll.js repeats a request while the page is visible: the next one 5 or 10 s after the previous answer, none while hidden, one at once when shown again. The Stats tab makes its charts once and polls only while shown. The answer handler runs after the poller's own bookkeeping, so an error in it (a chart for a disk added meanwhile) does not stop the polling. A Node test of poll.js runs with the test suite when node is installed.
This commit is contained in:
parent
5bb3a545a0
commit
731d14efd7
5 changed files with 260 additions and 73 deletions
|
|
@ -117,6 +117,7 @@
|
|||
{% endblock %}
|
||||
{% block script %}
|
||||
<script src="{% static "js/Chart.bundle.min.js" %}"></script>
|
||||
<script src="{% static "js/poll.js" %}"></script>
|
||||
<script>
|
||||
|
||||
var cpu_ctx = document.getElementById("cpuChart").getContext("2d");
|
||||
|
|
@ -219,29 +220,29 @@
|
|||
}
|
||||
});
|
||||
if (Boolean({{ status }}) === true) {
|
||||
window.setInterval(function graph_usage() {
|
||||
$.getJSON('{% url 'compute_graph' compute_id %}', function (data) {
|
||||
cpuChart.data.labels.push(data.timeline);
|
||||
memChart.data.labels.push(data.timeline);
|
||||
pollWhileVisible(5000, function () {
|
||||
return $.getJSON('{% url 'compute_graph' compute_id %}');
|
||||
}, function graph_usage(data) {
|
||||
cpuChart.data.labels.push(data.timeline);
|
||||
memChart.data.labels.push(data.timeline);
|
||||
|
||||
cpuChart.data.datasets[0].data.push(data.cpudata);
|
||||
if (cpuChart.data.datasets[0].data.length > 10){
|
||||
cpuChart.data.labels.shift();
|
||||
cpuChart.data.datasets[0].data.shift();
|
||||
}
|
||||
memChart.options.scales.yAxes[0].ticks.max = parseInt(data.memdata.total / 1048576);
|
||||
memChart.options.scales.yAxes[0].ticks.stepSize = parseInt(data.memdata.total / (1048576 * 5));
|
||||
memChart.data.datasets[0].data.push(parseInt(data.memdata.usage / 1048576));
|
||||
cpuChart.data.datasets[0].data.push(data.cpudata);
|
||||
if (cpuChart.data.datasets[0].data.length > 10){
|
||||
cpuChart.data.labels.shift();
|
||||
cpuChart.data.datasets[0].data.shift();
|
||||
}
|
||||
memChart.options.scales.yAxes[0].ticks.max = parseInt(data.memdata.total / 1048576);
|
||||
memChart.options.scales.yAxes[0].ticks.stepSize = parseInt(data.memdata.total / (1048576 * 5));
|
||||
memChart.data.datasets[0].data.push(parseInt(data.memdata.usage / 1048576));
|
||||
|
||||
if (memChart.data.datasets[0].data.length > 10){
|
||||
memChart.data.labels.shift();
|
||||
memChart.data.datasets[0].data.shift();
|
||||
}
|
||||
if (memChart.data.datasets[0].data.length > 10){
|
||||
memChart.data.labels.shift();
|
||||
memChart.data.datasets[0].data.shift();
|
||||
}
|
||||
|
||||
cpuChart.update();
|
||||
memChart.update();
|
||||
});
|
||||
}, 5000);
|
||||
cpuChart.update();
|
||||
memChart.update();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -378,9 +378,19 @@
|
|||
});
|
||||
</script>
|
||||
<script src="{% static 'js/Chart.bundle.min.js' %}"></script>
|
||||
<script src="{% static 'js/poll.js' %}"></script>
|
||||
<script>
|
||||
var stats_tab = document.querySelector('#graphics-tab');
|
||||
var stats_poll = null;
|
||||
// The charts are polled while the Stats tab is shown; they are made once.
|
||||
stats_tab.addEventListener('hidden.bs.tab', function (event) {
|
||||
stats_poll.stop();
|
||||
});
|
||||
stats_tab.addEventListener('shown.bs.tab', function (event) {
|
||||
if (stats_poll !== null) {
|
||||
stats_poll.start();
|
||||
return;
|
||||
}
|
||||
var cpu_ctx = $("#cpuChart").get(0).getContext("2d");
|
||||
var cpuChart = new Chart(cpu_ctx, {
|
||||
type: 'line',
|
||||
|
|
@ -597,69 +607,69 @@
|
|||
});
|
||||
{% endfor %}
|
||||
|
||||
var graph_interval = window.setInterval(function graph_usage() {
|
||||
$.getJSON("{% url 'instances:stats' instance.id %}", function (data) {
|
||||
cpuChart.data.labels.push(data.timeline);
|
||||
cpuChart.data.datasets[0].data.push(data.cpudata);
|
||||
if (cpuChart.data.datasets[0].data.length > 10){
|
||||
cpuChart.data.labels.shift();
|
||||
cpuChart.data.datasets[0].data.shift();
|
||||
stats_poll = pollWhileVisible(10000, function () {
|
||||
return $.getJSON("{% url 'instances:stats' instance.id %}");
|
||||
}, function graph_usage(data) {
|
||||
cpuChart.data.labels.push(data.timeline);
|
||||
cpuChart.data.datasets[0].data.push(data.cpudata);
|
||||
if (cpuChart.data.datasets[0].data.length > 10){
|
||||
cpuChart.data.labels.shift();
|
||||
cpuChart.data.datasets[0].data.shift();
|
||||
}
|
||||
cpuChart.update();
|
||||
|
||||
memChart.data.labels.push(data.timeline);
|
||||
memChart.options.scales.yAxes[0].ticks.max = parseInt(data.memdata.total / 1024);
|
||||
memChart.options.scales.yAxes[0].ticks.stepSize = parseInt(data.memdata.total / (1024 * 5));
|
||||
memChart.data.datasets[0].data.push(data.memdata.used / 1024);
|
||||
if (memChart.data.datasets[0].data.length > 10){
|
||||
memChart.data.labels.shift();
|
||||
memChart.data.datasets[0].data.shift();
|
||||
}
|
||||
memChart.update();
|
||||
|
||||
for (let j = 0; j < data.blkdata.length; j++) {
|
||||
diskChart[data.blkdata[j].dev].data.labels.push(data.timeline);
|
||||
|
||||
diskChart[data.blkdata[j].dev].data.datasets[0].data.push(data.blkdata[j].data[0]);
|
||||
diskChart[data.blkdata[j].dev].data.datasets[1].data.push(data.blkdata[j].data[1]);
|
||||
if (diskChart[data.blkdata[j].dev].data.datasets[0].data.length > 10){
|
||||
diskChart[data.blkdata[j].dev].data.labels.shift();
|
||||
diskChart[data.blkdata[j].dev].data.datasets[0].data.shift();
|
||||
diskChart[data.blkdata[j].dev].data.datasets[1].data.shift();
|
||||
}
|
||||
cpuChart.update();
|
||||
diskChart[data.blkdata[j].dev].update();
|
||||
}
|
||||
|
||||
memChart.data.labels.push(data.timeline);
|
||||
memChart.options.scales.yAxes[0].ticks.max = parseInt(data.memdata.total / 1024);
|
||||
memChart.options.scales.yAxes[0].ticks.stepSize = parseInt(data.memdata.total / (1024 * 5));
|
||||
memChart.data.datasets[0].data.push(data.memdata.used / 1024);
|
||||
if (memChart.data.datasets[0].data.length > 10){
|
||||
memChart.data.labels.shift();
|
||||
memChart.data.datasets[0].data.shift();
|
||||
for (let j = 0; j < data.netdata.length; j++) {
|
||||
netChart[data.netdata[j].dev].data.labels.push(data.timeline);
|
||||
|
||||
netChart[data.netdata[j].dev].data.datasets[0].data.push(data.netdata[j].data[0]);
|
||||
netChart[data.netdata[j].dev].data.datasets[1].data.push(data.netdata[j].data[1]);
|
||||
if (netChart[data.netdata[j].dev].data.datasets[0].data.length > 10){
|
||||
netChart[data.netdata[j].dev].data.labels.shift();
|
||||
netChart[data.netdata[j].dev].data.datasets[0].data.shift();
|
||||
netChart[data.netdata[j].dev].data.datasets[1].data.shift();
|
||||
}
|
||||
memChart.update();
|
||||
|
||||
for (let j = 0; j < data.blkdata.length; j++) {
|
||||
diskChart[data.blkdata[j].dev].data.labels.push(data.timeline);
|
||||
|
||||
diskChart[data.blkdata[j].dev].data.datasets[0].data.push(data.blkdata[j].data[0]);
|
||||
diskChart[data.blkdata[j].dev].data.datasets[1].data.push(data.blkdata[j].data[1]);
|
||||
if (diskChart[data.blkdata[j].dev].data.datasets[0].data.length > 10){
|
||||
diskChart[data.blkdata[j].dev].data.labels.shift();
|
||||
diskChart[data.blkdata[j].dev].data.datasets[0].data.shift();
|
||||
diskChart[data.blkdata[j].dev].data.datasets[1].data.shift();
|
||||
}
|
||||
diskChart[data.blkdata[j].dev].update();
|
||||
}
|
||||
|
||||
for (let j = 0; j < data.netdata.length; j++) {
|
||||
netChart[data.netdata[j].dev].data.labels.push(data.timeline);
|
||||
|
||||
netChart[data.netdata[j].dev].data.datasets[0].data.push(data.netdata[j].data[0]);
|
||||
netChart[data.netdata[j].dev].data.datasets[1].data.push(data.netdata[j].data[1]);
|
||||
if (netChart[data.netdata[j].dev].data.datasets[0].data.length > 10){
|
||||
netChart[data.netdata[j].dev].data.labels.shift();
|
||||
netChart[data.netdata[j].dev].data.datasets[0].data.shift();
|
||||
netChart[data.netdata[j].dev].data.datasets[1].data.shift();
|
||||
}
|
||||
netChart[data.netdata[j].dev].update();
|
||||
}
|
||||
});
|
||||
}, 10000);
|
||||
netChart[data.netdata[j].dev].update();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
backgroundJobRunning = false;
|
||||
var status_interval = window.setInterval(function get_status() {
|
||||
var status = {{ instance.status|lower }};
|
||||
$.getJSON("{% url 'instances:status' instance.id %}", function (data) {
|
||||
if (data['status'] != status && !backgroundJobRunning) {
|
||||
window.location.reload()
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
var status = {{ instance.status|lower }};
|
||||
var status_poll = pollWhileVisible(5000, function () {
|
||||
return $.getJSON("{% url 'instances:status' instance.id %}");
|
||||
}, function get_status(data) {
|
||||
if (data['status'] != status && !backgroundJobRunning) {
|
||||
window.location.reload()
|
||||
}
|
||||
});
|
||||
|
||||
// Stop getting status info before delete instance
|
||||
$('#delete_form').submit(function(){
|
||||
window.clearInterval(status_interval);
|
||||
status_poll.stop();
|
||||
return true;
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
50
static/js/poll.js
Normal file
50
static/js/poll.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Repeats request() while the page is visible and passes each answer to
|
||||
// onData. request() returns a jQuery promise (e.g. $.getJSON(url)); onData is
|
||||
// called after the poller's own bookkeeping, so an error in it does not stop
|
||||
// the polling. One request at a time: the next one starts
|
||||
// interval ms after the previous one has finished, so a slow host does not
|
||||
// pile up requests, each holding a web worker. A hidden page asks nothing;
|
||||
// when it is shown again it asks at once. stop() pauses, start() resumes
|
||||
// at once (for example while a tab of the page is shown).
|
||||
function pollWhileVisible(interval, request, onData) {
|
||||
var timer = null;
|
||||
var running = false;
|
||||
var stopped = false;
|
||||
|
||||
function run() {
|
||||
timer = null;
|
||||
if (stopped || document.hidden) {
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
request()
|
||||
.always(function () {
|
||||
running = false;
|
||||
if (!stopped && !document.hidden) {
|
||||
timer = setTimeout(run, interval);
|
||||
}
|
||||
})
|
||||
.done(onData);
|
||||
}
|
||||
|
||||
function resume() {
|
||||
if (!stopped && !document.hidden && !running && timer === null) {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("visibilitychange", resume);
|
||||
timer = setTimeout(run, interval);
|
||||
|
||||
return {
|
||||
stop: function () {
|
||||
stopped = true;
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
},
|
||||
start: function () {
|
||||
stopped = false;
|
||||
resume();
|
||||
},
|
||||
};
|
||||
}
|
||||
95
webvirtcloud/js_tests/poll.test.js
Normal file
95
webvirtcloud/js_tests/poll.test.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Behaviour of static/js/poll.js with a fake clock, document and jQuery promise.
|
||||
// Run by webvirtcloud/test_poll_js.py: node poll.test.js static/js/poll.js
|
||||
const fs = require("fs");
|
||||
const assert = require("assert");
|
||||
|
||||
let now = 0, timers = [], nextId = 1;
|
||||
global.setTimeout = (fn, ms) => { const id = nextId++; timers.push({ id, at: now + ms, fn }); return id; };
|
||||
global.clearTimeout = (id) => { timers = timers.filter((t) => t.id !== id); };
|
||||
function advance(ms) {
|
||||
const end = now + ms;
|
||||
for (;;) {
|
||||
timers.sort((a, b) => a.at - b.at);
|
||||
const t = timers[0];
|
||||
if (!t || t.at > end) break;
|
||||
timers.shift(); now = t.at; t.fn();
|
||||
}
|
||||
now = end;
|
||||
}
|
||||
const listeners = {};
|
||||
global.document = { hidden: false, addEventListener: (ev, fn) => { listeners[ev] = fn; } };
|
||||
function setHidden(h) { document.hidden = h; listeners.visibilitychange(); }
|
||||
|
||||
// requests: each returns a promise that the test settles. Like jQuery, it runs
|
||||
// the callbacks in the order they were added, and an exception in one stops
|
||||
// the ones after it.
|
||||
let requests = [];
|
||||
function request() {
|
||||
const callbacks = [];
|
||||
const r = {
|
||||
started: now,
|
||||
always(fn) { callbacks.push(fn); return r; },
|
||||
done(fn) { callbacks.push(fn); return r; },
|
||||
finish() {
|
||||
try { callbacks.forEach((fn) => fn({ ok: true })); } catch (e) { r.error = e; }
|
||||
},
|
||||
};
|
||||
requests.push(r);
|
||||
return r;
|
||||
}
|
||||
let answers = [];
|
||||
let failNext = false;
|
||||
function onData(data) {
|
||||
answers.push(data);
|
||||
if (failNext) { failNext = false; throw new Error("a chart for an unknown device"); }
|
||||
}
|
||||
eval(fs.readFileSync(process.argv[2], "utf8"));
|
||||
|
||||
const poll = pollWhileVisible(10000, request, onData);
|
||||
advance(9999); assert.strictEqual(requests.length, 0, "first request after the interval");
|
||||
advance(1); assert.strictEqual(requests.length, 1);
|
||||
|
||||
// a slow answer: no second request while the first is open
|
||||
advance(45000); assert.strictEqual(requests.length, 1, "no pile-up while a request is open");
|
||||
requests[0].finish();
|
||||
advance(9999); assert.strictEqual(requests.length, 1);
|
||||
advance(1); assert.strictEqual(requests.length, 2, "next one interval after the answer");
|
||||
requests[1].finish();
|
||||
|
||||
// hidden: nothing is asked
|
||||
setHidden(true);
|
||||
advance(60000); assert.strictEqual(requests.length, 2, "a hidden page asks nothing");
|
||||
// shown again: asks at once
|
||||
setHidden(false); assert.strictEqual(requests.length, 3, "asks at once when shown");
|
||||
requests[2].finish();
|
||||
advance(10000); assert.strictEqual(requests.length, 4);
|
||||
|
||||
// hidden while a request is open: no new one when it finishes, one on show
|
||||
setHidden(true); requests[3].finish();
|
||||
advance(60000); assert.strictEqual(requests.length, 4);
|
||||
setHidden(false); assert.strictEqual(requests.length, 5);
|
||||
|
||||
// briefly hidden while waiting: the pending timer still fires once, no double request
|
||||
requests[4].finish();
|
||||
setHidden(true); setHidden(false);
|
||||
advance(10000); assert.strictEqual(requests.length, 6, "no extra request for a short hide");
|
||||
|
||||
// stop: nothing more, also not on show
|
||||
poll.stop(); requests[5].finish();
|
||||
setHidden(true); setHidden(false); advance(60000);
|
||||
assert.strictEqual(requests.length, 6, "stopped");
|
||||
// start after stop: asks at once, then every interval
|
||||
poll.start(); assert.strictEqual(requests.length, 7, "start asks at once");
|
||||
requests[6].finish(); advance(10000); assert.strictEqual(requests.length, 8);
|
||||
// start while running or waiting adds nothing
|
||||
poll.start(); assert.strictEqual(requests.length, 8);
|
||||
requests[7].finish(); poll.start(); advance(9999); assert.strictEqual(requests.length, 8, "start while waiting");
|
||||
// start while hidden asks nothing until shown
|
||||
poll.stop(); setHidden(true); poll.start(); assert.strictEqual(requests.length, 8);
|
||||
setHidden(false); assert.strictEqual(requests.length, 9);
|
||||
// an error in onData does not stop the polling
|
||||
assert.strictEqual(answers.length, 8, "each answer reaches onData");
|
||||
failNext = true; requests[8].finish();
|
||||
assert.ok(requests[8].error, "the error reaches the page");
|
||||
advance(10000); assert.strictEqual(requests.length, 10, "polling goes on after an error in onData");
|
||||
console.log("poll.js: all checks passed");
|
||||
31
webvirtcloud/test_poll_js.py
Normal file
31
webvirtcloud/test_poll_js.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""Pages poll the server through static/js/poll.js: one request at a time,
|
||||
none while the page is hidden. Its behaviour is checked with Node."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
BASE = Path(settings.BASE_DIR)
|
||||
POLLING_TEMPLATES = ["instances/templates/instance.html", "computes/templates/overview.html"]
|
||||
|
||||
|
||||
class PollTestCase(unittest.TestCase):
|
||||
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
||||
def test_poll_js(self):
|
||||
result = subprocess.run(
|
||||
["node", str(BASE / "webvirtcloud/js_tests/poll.test.js"), str(BASE / "static/js/poll.js")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_pages_poll_through_poll_js(self):
|
||||
for template in POLLING_TEMPLATES:
|
||||
with self.subTest(template=template):
|
||||
source = (BASE / template).read_text()
|
||||
self.assertNotIn("setInterval", source)
|
||||
self.assertIn("pollWhileVisible(", source)
|
||||
Loading…
Add table
Add a link
Reference in a new issue