From 24231813d7e7927a42c2d03d9468a07d045f2c32 Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 9 Oct 2026 11:22:33 +0300 Subject: [PATCH] perf(instances): the VM stats no longer wait a second per poll Each stats answer carries a signed sample of the counters, bound to the VM, its compute and UUID. The next poll sends it back and the rates are the averages since then; a sample from another run of the VM (domain ID), another host CPU count or device list, or older than 30 s falls back to one second as before. Windows are measured on the monotonic clock. Now that a poll is cheap, the Stats tab polls every 5 s like the host graph, the first time as soon as it opens (pollWhileVisible takes the delay of the first request). A counter the host reports as unsupported (-1) or that went back gives no value instead of 0 or a jump; a NIC without a target keeps its chart, a pool volume disk is measured, and a disk or NIC attached after the page loaded no longer stops the other charts. Disk rates are MB/s (they were MiB/s labelled Mb/s), network Mbit/s; the CPU tooltip names the window. --- README.md | 1 + instances/templates/instance.html | 62 ++++----- instances/test_stats_sample.py | 194 +++++++++++++++++++++++++++++ instances/views.py | 64 +++++----- static/js/poll.js | 7 +- vrtManager/instance.py | 137 ++++++++++++-------- vrtManager/test_usage.py | 179 ++++++++++++++++++++------ webvirtcloud/js_tests/poll.test.js | 9 ++ 8 files changed, 501 insertions(+), 152 deletions(-) create mode 100644 instances/test_stats_sample.py diff --git a/README.md b/README.md index 3c55774b..6d49639b 100644 --- a/README.md +++ b/README.md @@ -535,6 +535,7 @@ A rollback leaves the clone on a detached HEAD at the restored commit; the updat - The log rotates (10 MB × 5) in `data/webvirtcloud.log`. - A disk can be grown while its VM runs or is paused, not only when it is shut off. - A host's CPU graph shows the average since its previous point (normally 5 s) instead of a 1 s sample taken while a worker waits. +- A VM's stats are polled every 5 s like the host graph, the first point as soon as the tab opens; each point is the average since the previous one, disk rates in MB/s (they were MiB/s labelled Mb/s); a counter the host does not report shows a gap instead of 0. - A running VM without a guest agent shows the IPv4 address the host learned by ARP (the lookup always failed before). - After a live migration the VM's NVRAM file is deleted on the source, when it is in libvirt's `/var/lib/libvirt/qemu/nvram` and the destination does not share that directory (a VM created there later with the same name would start with those UEFI variables). - gstfsd finds the guest's operating system the way libguestfs inspects it, so a root on LVM works, and it always answers (an error when it finds no or several operating systems). Update `/usr/local/bin/gstfsd` on every compute (see [Adding a Compute Node](#adding-a-compute-node), step 3). diff --git a/instances/templates/instance.html b/instances/templates/instance.html index 0dc5fc73..aebf4da7 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -381,8 +381,13 @@ ", self.page(), re.S) if "statsSample" in s) + js = """ + const vm = require("vm"); + const charts = [], requests = [], handlers = {}; + let poll, fail = false; + const canvas = {get: () => ({getContext: () => ({})})}; + const $ = () => canvas; + $.getJSON = (url, params) => { + requests.push(params); + return {fail: (callback) => { if (fail) callback(); return {}; }}; + }; + const page = { + $, + document: {querySelector: () => ({addEventListener: (name, handler) => { handlers[name] = handler; }})}, + // Chart.js adds the lists a config leaves out + Chart: function (ctx, config) { + this.data = Object.assign({labels: []}, config.data); + this.data.datasets.forEach((set) => { set.data = set.data || []; }); + this.options = config.options; + this.update = () => {}; + charts.push(this); + }, + pollWhileVisible: (interval, request, onData, first) => { poll = {interval, first, request, onData}; return {}; }, + }; + vm.runInNewContext(SCRIPT, vm.createContext(page)); + handlers["shown.bs.tab"](); + const answer = (cpu, sample) => ({cpudata: cpu, window: 9.9, sample: sample, timeline: "t", + memdata: {used: 0, total: 1024}, + // vdb was attached after the page loaded + blkdata: [{dev: "vdb", data: [9, 9]}, {dev: "vda", data: [1.5, null]}], + netdata: [{dev: 0, data: [2, 3]}]}); + poll.request(); + poll.onData(answer(3, "token-1")); + poll.request(); + fail = true; + poll.request(); + fail = false; + poll.request(); + const label = charts[0].options.tooltips.callbacks.label({datasetIndex: 0, index: 0, yLabel: 3}, charts[0].data); + console.log(JSON.stringify({requests, label, every: [poll.interval, poll.first], cpu: charts[0].data.datasets[0].data, + disk: charts[2].data.datasets[1].data, net: charts[3].data.datasets[0].data})); + """.replace("SCRIPT", json.dumps(script)) + out = subprocess.run(["node", "-e", js], capture_output=True, text=True, timeout=30) + self.assertEqual(out.returncode, 0, out.stderr) + result = json.loads(out.stdout) + # the first poll at once, then every 5 s + self.assertEqual(result["every"], [5000, 0]) + # the sample goes with the next poll; a failed poll drops it + self.assertEqual(result["requests"], [{}, {"sample": "token-1"}, {"sample": "token-1"}, {}]) + # a disk without a chart is skipped, the other charts still update + self.assertEqual((result["cpu"], result["disk"], result["net"]), ([3], [None], [2])) + self.assertTrue(result["label"].endswith(": 3 % (average over 9.9 s)"), result["label"]) diff --git a/instances/views.py b/instances/views.py index b8ca3c65..f78d9aac 100755 --- a/instances/views.py +++ b/instances/views.py @@ -22,6 +22,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import User +from django.core import signing from django.core.exceptions import PermissionDenied, ValidationError from django.http import Http404, HttpResponse, JsonResponse from django.db.models import prefetch_related_objects @@ -41,7 +42,7 @@ from logs.views import addlogmsg from webvirtcloud.middleware import error_text from vrtManager import util from vrtManager.create import wvmCreate -from vrtManager.instance import wvmInstances +from vrtManager.instance import USAGE_MAX_WINDOW, wvmInstances from vrtManager.interface import wvmInterface from vrtManager.storage import wvmStorage from vrtManager.util import randomPasswd @@ -54,6 +55,8 @@ from .models import Flavor # the VM page waits this long (seconds) for the compute's drbdadm status DRBD_STATUS_TIMEOUT = 10 +# the stats tab sends the sample of each answer back with its next poll +STATS_SAMPLE_SIGNER = signing.TimestampSigner(salt="instances.stats-sample") def index(request): @@ -269,41 +272,42 @@ def drbd_status(request, pk): return result +def _stats_sample(instance, token): + """The usage sample in a token of this VM's stats on its current compute, + or None when there is none, it is altered or too old.""" + if not token: + return None + try: + data = STATS_SAMPLE_SIGNER.unsign_object(token, max_age=USAGE_MAX_WINDOW) + except signing.BadSignature: + return None + if data.get("vm") != [instance.id, instance.compute_id, instance.uuid]: + return None + return data["sample"] + + def stats(request, pk): instance = get_instance(request.user, pk) - json_blk = [] - json_net = [] - - # TODO: stats are inaccurate - cpu_usage, blk_usage, net_usage = instance.proxy.usage() + usage = instance.proxy.usage(_stats_sample(instance, request.GET.get("sample"))) mem_usage = instance.proxy.mem_usage() - current_time = time.strftime("%H:%M:%S") - for blk in blk_usage: - json_blk.append( - { - "dev": blk["dev"], - "data": [int(blk["rd"]) / 1048576, int(blk["wr"]) / 1048576], - } - ) + def mega(value): + return None if value is None else value / 10**6 - for net in net_usage: - json_net.append( - { - "dev": net["dev"], - "data": [int(net["rx"]) / 1048576, int(net["tx"]) / 1048576], - } + result = { + "cpudata": None if usage["cpu"] is None else round(usage["cpu"], 1), + "memdata": mem_usage, + # MB/s and Mbit/s + "blkdata": [{"dev": disk["dev"], "data": [mega(disk["rd"]), mega(disk["wr"])]} for disk in usage["disks"]], + "netdata": [{"dev": nic["dev"], "data": [mega(nic["rx"]), mega(nic["tx"])]} for nic in usage["nics"]], + "timeline": time.strftime("%H:%M:%S"), + } + if "sample" in usage: + result["window"] = None if usage["window"] is None else round(usage["window"], 1) + result["sample"] = STATS_SAMPLE_SIGNER.sign_object( + {"vm": [instance.id, instance.compute_id, instance.uuid], "sample": usage["sample"]}, compress=True ) - - return JsonResponse( - { - "cpudata": int(cpu_usage["cpu"]), - "memdata": mem_usage, - "blkdata": json_blk, - "netdata": json_net, - "timeline": current_time, - } - ) + return JsonResponse(result) def osinfo(request, pk): diff --git a/static/js/poll.js b/static/js/poll.js index 91b12fb5..113ee05c 100644 --- a/static/js/poll.js +++ b/static/js/poll.js @@ -5,8 +5,9 @@ // 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) { +// at once (for example while a tab of the page is shown). The first request +// goes out after firstDelay ms, the interval if it is not given. +function pollWhileVisible(interval, request, onData, firstDelay) { var timer = null; var running = false; var stopped = false; @@ -34,7 +35,7 @@ function pollWhileVisible(interval, request, onData) { } document.addEventListener("visibilitychange", resume); - timer = setTimeout(run, interval); + timer = setTimeout(run, firstDelay === undefined ? interval : firstDelay); return { stop: function () { diff --git a/vrtManager/instance.py b/vrtManager/instance.py index a92f1841..67ff8cb0 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -60,6 +60,16 @@ from vrtManager.storage import wvmStorage, wvmStorages # with secrets. Live XML would drop pending changes, non-secure XML the VNC # password. PERSISTENT_XML = VIR_DOMAIN_XML_INACTIVE | VIR_DOMAIN_XML_SECURE +# the longest window wvmInstance.usage() averages over +USAGE_MAX_WINDOW = 30 + + +def _rate(before, after, seconds, scale=1): + """Growth per second of a counter, or None when a reading is unsupported + (-1) or the counter went back.""" + if before < 0 or after < before: + return None + return scale * (after - before) / seconds class wvmInstances(wvmConnect): @@ -1266,68 +1276,91 @@ class wvmInstance(wvmConnect): mem_usage["total"] = 0 return mem_usage - def usage(self): + def usage(self, previous=None): """ CPU (percent of the host), disk read/write bytes and network rx/tx - bits of the last second, for the stats view. All counters are read - before and after one shared second: the view runs in a web worker, - and a second per device kept it busy for 1 + disks + NICs seconds. + bits per second, for the stats view. With previous, the "sample" of an + earlier answer, they are averages since then and nothing waits; without + a usable one, over one second. A sample is usable for the same run of + the VM (domain ID) on a host with the same CPUs and devices, at most + USAGE_MAX_WINDOW seconds old. A value the host does not report (-1) or + whose counter went back is None. """ - devices = [] tree = ElementTree.fromstring(self._XMLDesc(0)) + disks = [] for disk in tree.findall("devices/disk"): - if disk.get("device") == "disk": - dev_file = None - dev_bus = None - network_disk = True - for elm in disk: - if elm.tag == "source": - if elm.get("protocol"): - dev_file = elm.get("protocol") - network_disk = True - if elm.get("file"): - dev_file = elm.get("file") - if elm.get("dev"): - dev_file = elm.get("dev") - if elm.tag == "target": - dev_bus = elm.get("dev") - if (dev_file and dev_bus) is not None: - if network_disk: - dev_file = dev_bus - devices.append([dev_file, dev_bus]) + source, target = disk.find("source"), disk.find("target") + if ( + disk.get("device") == "disk" + and source is not None + and any(source.get(name) for name in ("protocol", "file", "dev", "volume")) + and target is not None + and target.get("dev") + ): + disks.append(target.get("dev")) # blockStats takes the target name + # all NICs, in the order of the page's charts; one without a target is not measured nics = [] - running = self.get_status() == 1 - if running: - tree = ElementTree.fromstring(self._XMLDesc(0)) - for target in tree.findall("devices/interface/target"): - nics.append(target.get("dev")) + for nic in tree.findall("devices/interface"): + mac, target = nic.find("mac"), nic.find("target") + nics.append([ + "" if mac is None else mac.get("address", ""), + "" if target is None else target.get("dev", ""), + ]) - def sample(): - return ( + if self.get_status() != 1: + return { + "cpu": 0, + "disks": [{"dev": dev, "rd": 0, "wr": 0} for dev in disks], + "nics": [{"dev": i, "rx": 0, "tx": 0} for i in range(len(nics))], + } + + key = [self.instance.ID(), self._host_info()[2], disks, nics] + unread = (-1,) * 8 + + def read(): + blocks = [self.instance.blockStats(dev) for dev in disks] + links = [self.instance.interfaceStats(target) if target else unread for _, target in nics] + return [ self.instance.info()[4], - [self.instance.blockStats(dev[0]) for dev in devices], - [self.instance.interfaceStats(dev) for dev in nics], - ) + [[stats[1], stats[3]] for stats in blocks], + [[stats[0], stats[4]] for stats in links], + time.monotonic(), + ] - cpu_usage = {"cpu": 0} - dev_usage = [{"dev": dev[1], "rd": 0, "wr": 0} for dev in devices] - if not running: - net_usage = [{"dev": i, "rx": 0, "tx": 0} for i, _ in enumerate(self.get_net_devices())] - return cpu_usage, dev_usage, net_usage + def rates(before, after): + seconds = after[3] - before[3] + if before[0] > after[0] or not 0 < seconds <= USAGE_MAX_WINDOW: + return None + return { + "cpu": 100 * (after[0] - before[0]) / (seconds * key[1] * 10**9), + "disks": [ + {"dev": dev, "rd": _rate(old[0], new[0], seconds), "wr": _rate(old[1], new[1], seconds)} + for dev, old, new in zip(disks, before[1], after[1]) + ], + "nics": [ + {"dev": i, "rx": _rate(old[0], new[0], seconds, 8), "tx": _rate(old[1], new[1], seconds, 8)} + for i, (old, new) in enumerate(zip(before[2], after[2])) + ], + "window": seconds, + } - nbcore = self.wvm.getInfo()[2] - cpu_ago, blk_ago, net_ago = sample() - time.sleep(1) - cpu_now, blk_now, net_now = sample() - cpu_usage["cpu"] = 100 * (cpu_now - cpu_ago) / (1 * nbcore * 10**9) - for usage, ago, now in zip(dev_usage, blk_ago, blk_now): - usage["rd"] = now[1] - ago[1] - usage["wr"] = now[3] - ago[3] - net_usage = [ - {"dev": i, "rx": (now[0] - ago[0]) * 8, "tx": (now[4] - ago[4]) * 8} - for i, (ago, now) in enumerate(zip(net_ago, net_now)) - ] - return cpu_usage, dev_usage, net_usage + counters = read() + usage = None + if previous is not None and previous.get("key") == key: + usage = rates(previous["counters"], counters) + if usage is None: + before = counters + time.sleep(1) + counters = read() + # None everywhere if the VM restarted meanwhile + usage = rates(before, counters) or { + "cpu": None, + "disks": [{"dev": dev, "rd": None, "wr": None} for dev in disks], + "nics": [{"dev": i, "rx": None, "tx": None} for i in range(len(nics))], + "window": None, + } + usage["sample"] = {"key": key, "counters": counters} + return usage def get_telnet_port(self): telnet_port = None diff --git a/vrtManager/test_usage.py b/vrtManager/test_usage.py index 70782407..ca25b62a 100644 --- a/vrtManager/test_usage.py +++ b/vrtManager/test_usage.py @@ -1,5 +1,7 @@ -"""VM statistics sample CPU, disks and NICs over one shared second: the stats -view runs in a sync worker, polled every 10 s by each open VM page.""" +"""VM statistics: rates are averages since the sample of the previous answer, +without waiting, when it is from the same run of the VM on the same host and +devices; otherwise over one second. The stats view runs in a sync worker, +polled every 5 s by each open VM page.""" import unittest from unittest.mock import patch @@ -9,69 +11,168 @@ from django.conf import settings if not settings.configured: settings.configure(MAC_OUI="52:54:10") -from vrtManager.instance import wvmInstance +from vrtManager.instance import USAGE_MAX_WINDOW, wvmInstance XML = """ - + - + + """ class FakeDomain: - """Counters grow by a fixed amount per elapsed second (per sleep).""" + """Counters grow by a fixed amount per second of the clock.""" - def __init__(self, state): + def __init__(self, state, clock): self.state = state - self.second = 0 + self.clock = clock + self.domain_id = 7 + self.unsupported = False + + def now(self): + return self.clock[0] + + def ID(self): + return self.domain_id def info(self): - return [self.state, 0, 0, 2, self.second * 500_000_000] # 0.5 s of CPU time per second + return [self.state, 0, 0, 2, int(self.now() * 500_000_000)] # 0.5 s of CPU time per second def blockStats(self, dev): n = {"vda": 1, "vdb": 2}[dev] # disks are read by target name - return (0, self.second * n * 1000, 0, self.second * n * 3000, 0) + written = -1 if self.unsupported and dev == "vdb" else int(self.now() * n * 3000) + return (0, int(self.now() * n * 1000), 0, written, 0) def interfaceStats(self, dev): - return (self.second * 100, 0, 0, 0, self.second * 50, 0, 0, 0) + assert dev == "vnet0" + return (int(self.now() * 100), 0, 0, 0, int(self.now() * 50), 0, 0, 0) -class FakeConn: - def getInfo(self): - return ["x86_64", 0, 4] # 4 host CPUs - - -def instance(state): +def instance(state, clock): proxy = wvmInstance.__new__(wvmInstance) # no connection: only these calls - proxy.instance = FakeDomain(state) - proxy.wvm = FakeConn() + proxy._read_cache = None + proxy.instance = FakeDomain(state, clock) proxy._XMLDesc = lambda flags: XML - proxy.get_net_devices = lambda: [{"mac": "52:54:00:00:00:01"}] + proxy._host_info = lambda: ["x86_64", 0, 4] # 4 host CPUs + proxy.get_status = lambda: state return proxy class UsageTestCase(unittest.TestCase): - def sample(self, state): - proxy = instance(state) + def setUp(self): + self.clock = [100.0] + patcher = patch("vrtManager.instance.time") + self.addCleanup(patcher.stop) + self.time = patcher.start() + self.time.monotonic.side_effect = lambda: self.clock[0] + self.time.sleep.side_effect = self.advance - def sleep(seconds): - proxy.instance.second += seconds + def advance(self, seconds): + self.clock[0] += seconds - with patch("vrtManager.instance.time.sleep", side_effect=sleep) as slept: - result = proxy.usage() - return result, slept + def usage(self, proxy, previous=None): + result = proxy.usage(previous) + self.time.sleep.reset_mock() + return result - def test_a_running_vm_is_sampled_over_one_second(self): - (cpu, disks, nets), slept = self.sample(1) - slept.assert_called_once_with(1) - self.assertEqual(cpu, {"cpu": 12.5}) # 0.5 s of 4 host CPUs - self.assertEqual(disks, [{"dev": "vda", "rd": 1000, "wr": 3000}, {"dev": "vdb", "rd": 2000, "wr": 6000}]) - self.assertEqual(nets, [{"dev": 0, "rx": 800, "tx": 400}]) # bits + def test_without_a_sample_over_one_second(self): + proxy = instance(1, self.clock) + result = proxy.usage() + self.time.sleep.assert_called_once_with(1) + self.assertEqual((result["cpu"], result["window"]), (12.5, 1.0)) # 0.5 s of 4 host CPUs + # a pool volume is measured like a file + self.assertEqual( + result["disks"], [{"dev": "vda", "rd": 1000, "wr": 3000}, {"dev": "vdb", "rd": 2000, "wr": 6000}] + ) + # bits; the NIC without a target is not measured and keeps its place + self.assertEqual(result["nics"], [{"dev": 0, "rx": None, "tx": None}, {"dev": 1, "rx": 800, "tx": 400}]) + self.assertEqual(result["sample"]["counters"][3], 101.0) - def test_a_vm_that_is_not_running_is_not_sampled(self): - (cpu, disks, nets), slept = self.sample(5) - slept.assert_not_called() - self.assertEqual(cpu, {"cpu": 0}) - self.assertEqual(disks, [{"dev": "vda", "rd": 0, "wr": 0}, {"dev": "vdb", "rd": 0, "wr": 0}]) - self.assertEqual(nets, [{"dev": 0, "rx": 0, "tx": 0}]) + def test_since_the_previous_sample_without_waiting(self): + proxy = instance(1, self.clock) + sample = self.usage(proxy)["sample"] + self.advance(9.5) + result = proxy.usage(sample) + self.time.sleep.assert_not_called() + self.assertEqual((result["cpu"], result["window"]), (12.5, 9.5)) + self.assertEqual(result["disks"][1], {"dev": "vdb", "rd": 2000, "wr": 6000}) # per second + self.assertEqual(result["nics"][1], {"dev": 1, "rx": 800, "tx": 400}) + self.assertEqual(result["sample"]["counters"][3], 110.5) + + def test_a_sample_of_another_run_host_or_devices_is_not_used(self): + def restarted(proxy): + proxy.instance.domain_id = 8 + + def other_host_cpus(proxy): + proxy._host_info = lambda: ["x86_64", 0, 8] + + def nic_renamed(proxy): + proxy._XMLDesc = lambda flags: XML.replace("vnet0", "vnet1") + + for change in (restarted, other_host_cpus, nic_renamed): + with self.subTest(change.__name__): + proxy = instance(1, self.clock) + sample = self.usage(proxy)["sample"] + change(proxy) + self.advance(5) + proxy.instance.interfaceStats = lambda dev: (0,) * 8 + self.assertEqual(proxy.usage(sample)["window"], 1.0) + self.time.sleep.assert_called_once_with(1) + self.time.sleep.reset_mock() + + def test_a_sample_too_old_from_the_future_or_counting_back_is_not_used(self): + for name, seconds in (("too old", USAGE_MAX_WINDOW + 1), ("future", -5), ("same time", 0)): + with self.subTest(name): + proxy = instance(1, self.clock) + sample = self.usage(proxy)["sample"] + self.advance(seconds) + self.assertEqual(proxy.usage(sample)["window"], 1.0) + self.time.sleep.assert_called_once_with(1) + self.time.sleep.reset_mock() + proxy = instance(1, self.clock) + sample = self.usage(proxy)["sample"] + sample["counters"][0] += 10**12 # CPU time went back: a restart with the same domain ID + self.advance(5) + self.assertEqual(proxy.usage(sample)["cpu"], 12.5) + self.time.sleep.assert_called_once_with(1) + + def test_a_counter_the_host_does_not_report_has_no_value(self): + proxy = instance(1, self.clock) + proxy.instance.unsupported = True + disks = proxy.usage()["disks"] + self.assertEqual(disks[1], {"dev": "vdb", "rd": 2000, "wr": None}) + + def test_a_vm_restarted_during_the_second_has_no_values(self): + proxy = instance(1, self.clock) + domain = proxy.instance + real_sleep = self.time.sleep.side_effect + + def restart(seconds): + real_sleep(seconds) + domain.info = lambda: [1, 0, 0, 2, 0] # CPU time starts over + + self.time.sleep.side_effect = restart + result = proxy.usage() + self.assertEqual((result["cpu"], result["window"]), (None, None)) + self.assertEqual(result["disks"][0], {"dev": "vda", "rd": None, "wr": None}) + self.assertIn("sample", result) + + def test_a_paused_or_shut_off_vm_is_not_sampled(self): + for state in (3, 5): + with self.subTest(state=state): + result = instance(state, self.clock).usage() + self.time.sleep.assert_not_called() + self.assertEqual( + result, + { + "cpu": 0, + "disks": [{"dev": "vda", "rd": 0, "wr": 0}, {"dev": "vdb", "rd": 0, "wr": 0}], + "nics": [{"dev": 0, "rx": 0, "tx": 0}, {"dev": 1, "rx": 0, "tx": 0}], + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/webvirtcloud/js_tests/poll.test.js b/webvirtcloud/js_tests/poll.test.js index 54a90f7b..e49fae0b 100644 --- a/webvirtcloud/js_tests/poll.test.js +++ b/webvirtcloud/js_tests/poll.test.js @@ -92,4 +92,13 @@ 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"); + +// a first delay of 0: the first request at once, then every interval +poll.stop(); requests[9].finish(); +const atOnce = pollWhileVisible(5000, request, onData, 0); +advance(0); assert.strictEqual(requests.length, 11, "first request after firstDelay"); +requests[10].finish(); +advance(4999); assert.strictEqual(requests.length, 11); +advance(1); assert.strictEqual(requests.length, 12, "then every interval"); +atOnce.stop(); console.log("poll.js: all checks passed");