Change in vdsm[master]: virt: sampling: add bulk stats implementation

fromani at redhat.com fromani at redhat.com
Fri Jan 9 09:44:12 UTC 2015


Francesco Romani has uploaded a new change for review.

Change subject: virt: sampling: add bulk stats implementation
......................................................................

virt: sampling: add bulk stats implementation

Add support for libvirt bluk stats.
The new implementation is parallel to the old
one, completely backward compatible to surrounding
code and switchable using a configuration item.

The new code has *soft* dependency to libvirt
to get bulk stats.
Thread usage drops from 1xVM to constant number
(2 at the moment)

The new code improves performance due to both massive
thread reduction in the VDSM process and
to usage of more efficient API.

The new code has detection of stuck VMs to improve response
time and to properly mark as such unresponsive
VMs.

Please note that this patch intentionally leave
out the disk bookkeeping (_highWrite, _updateVolume)
which will be addressed by followup patches.

Change-Id: Ia2f1a62515059e663b418c6c7b61a15881980dc9
Signed-off-by: Francesco Romani <fromani at redhat.com>
---
M lib/vdsm/config.py.in
M vdsm/vdsm
M vdsm/virt/sampling.py
M vdsm/virt/vm.py
4 files changed, 560 insertions(+), 28 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/vdsm refs/changes/22/36722/1

diff --git a/lib/vdsm/config.py.in b/lib/vdsm/config.py.in
index 94c6782..aa9e7f5 100644
--- a/lib/vdsm/config.py.in
+++ b/lib/vdsm/config.py.in
@@ -145,6 +145,9 @@
         ('vm_watermark_interval', '2',
             'How often should we sample each vm for statistics (seconds).'),
 
+        ('vm_sample_bulk', 'false',
+            'Use libvirt bulk stats to sampling. Requires libvirt >= 1.2.9'),
+
         ('vm_sample_cpu_interval', '15', None),
 
         ('vm_sample_cpu_window', '2', None),
diff --git a/vdsm/vdsm b/vdsm/vdsm
index 5c1ab71..131d488 100755
--- a/vdsm/vdsm
+++ b/vdsm/vdsm
@@ -39,6 +39,8 @@
 from storage.dispatcher import Dispatcher
 from storage.hsm import HSM
 
+from virt import sampling
+
 import vdsm.infra.zombiereaper as zombiereaper
 import dsaversion
 
@@ -88,12 +90,14 @@
     from clientIF import clientIF  # must import after config is read
     cif = clientIF.getInstance(irs, log)
     cif.start()
+    sampling.start(cif)
     try:
         while running[0]:
             signal.pause()
 
         profile.stop()
     finally:
+        sampling.stop()
         cif.prepareForShutdown()
 
 
diff --git a/vdsm/virt/sampling.py b/vdsm/virt/sampling.py
index c484ab3..a74eb62 100644
--- a/vdsm/virt/sampling.py
+++ b/vdsm/virt/sampling.py
@@ -30,10 +30,18 @@
 import threading
 import time
 
+import libvirt
+
+from vdsm.config import config
 from vdsm.constants import P_VDSM_RUN, P_VDSM_CLIENT_LOG
+from vdsm import executor
 from vdsm import ipwrapper
+from vdsm import libvirtconnection
 from vdsm import netinfo
+from vdsm import schedule
 from vdsm import utils
+
+from .utils import ExpiringSet
 
 import caps
 
@@ -42,6 +50,156 @@
     _THP_STATE_PATH = '/sys/kernel/mm/redhat_transparent_hugepage/enabled'
 
 
+_SAMPLING_THREADS = 2  # one is the minimum, let's have a spare one
+_SAMPLING_TASKS = 100  # just 'high enough' number
+
+
+_scheduler = None
+
+_executor = None
+
+_sampler = None
+
+
+def _dispatch_stats(vm, bulkStats):
+    # may be None due a benign race with the creation thread.
+    # Don't worry and go ahead.
+    if vm.stats:
+        vm.stats.sampleBulk.append(bulkStats)
+        vm.stats.last_sample_time = time.time()
+
+
+def _collect_stats(vm, bulkStats):
+    vm.stats.sampleVmJobs()
+    vm.stats.sampleCpuTune()
+
+
+class Sampler(object):
+    STUCK_TIMEOUT = 30  # seconds
+
+    SAMPLE_TIMEOUT = 1  # seconds
+
+    SAMPLE_DELAY = 2  # seconds. must be > SAMPLE_TIMEOUT
+
+    def __init__(self, cif,
+                 conn=None, scheduler=_scheduler, executor=_executor):
+        self._cif = cif
+        self._conn = libvirtconnection.get(cif) if conn is None else conn
+        self._scheduler = scheduler
+        self._executor = executor
+        self._skip_doms = ExpiringSet(self.STUCK_TIMEOUT)
+        self._log = logging.getLogger("sampling.sampler")
+        self._lock = threading.Lock()
+        self._running = False
+        self._call = None
+
+    def start(self):
+        with self._lock:
+            if self._running:
+                raise AssertionError("Sampler is running")
+            self._log.debug("Starting sampler")
+            self._running = True
+            self._step()
+
+    def stop(self):
+        with self._lock:
+            if self._running:
+                self._log.debug("Stopping sampler")
+                self._running = False
+                if self._call:
+                    self._call.cancel()
+                    self._call = None
+                self._iterator = None
+
+    # Task interface
+
+    def __call__(self):
+        try:
+            doms, vms = self._get_domains_and_vms()
+            self._log.debug('sampling %d domains - %d skipped',
+                            len(doms), len(self._skip_doms))
+            allStats = self._conn.domainListGetStats(doms)
+            self._log.debug('got %d stats', len(allStats))
+            self._put_stats_into_vms(vms, allStats)
+        except Exception as e:
+            self._log.exception("Stats function failed")
+        finally:
+            with self._lock:
+                if self._running:
+                    self._step()
+
+    # Private
+
+    def _get_domains_and_vms(self):
+        self._skip_doms.update()
+
+        doms = []
+        vms = self._cif.getVMs()
+        for vmId, vmObj in vms.iteritems():
+            if vmId in self._skip_doms:
+                continue
+            elif not vmObj.isDomainReady():
+                self._skip_doms.add(vmId)
+                vmObj.setUnresponsive()
+            else:
+                doms.append(vmObj._libvirtDom)
+
+        return doms, vms
+
+    def _put_stats_into_vms(self, vms, allStats):
+        for dom, stats in allStats:
+            vmObj = vms[dom.UUIDString()]
+
+            self._skip_doms.remove(vmObj.id)
+            vmObj.setResponsive()
+
+            _dispatch_stats(vmObj, stats)
+            # special case handled last
+            _collect_stats(vmObj, stats)
+
+    def _dispatch(self):
+        """
+        Called from the scheduler thread when its time to run the current
+        sampling task.
+        """
+        with self._lock:
+            if self._running:
+                self._call = None
+                self._executor.dispatch(self, timeout=self.SAMPLE_TIMEOUT)
+
+    def _step(self):
+        self._log.debug('after %d seconds: _dispatch', self.SAMPLE_DELAY)
+        self._call = self._scheduler.schedule(self.DELAY, self._dispatch)
+
+
+def start(cif):
+    """ Called during application startup """
+    global _executor
+    global _sampler
+    global _scheduler
+
+    if config.getboolean('vars', 'vm_sample_bulk'):
+        _scheduler = schedule.Scheduler("sampling.scheduler")
+        _scheduler.start()
+
+        _executor = executor.Executor(
+            _SAMPLING_THREADS, _SAMPLING_TASKS, _scheduler)
+        _executor.start()
+
+        _sampler = Sampler(cif)
+        _sampler.start()
+
+
+def stop():
+    """ Called during application shutdown """
+    if _sampler:
+        _sampler.stop()
+    if _executor:
+        _executor.stop(wait=True)
+    if _scheduler:
+        _scheduler.stop()
+
+
 class InterfaceSample(object):
     """
     A network interface sample.
diff --git a/vdsm/virt/vm.py b/vdsm/virt/vm.py
index fb46a54..da736bd 100644
--- a/vdsm/virt/vm.py
+++ b/vdsm/virt/vm.py
@@ -67,7 +67,7 @@
 from .vmtune import io_tune_values_to_dom, io_tune_dom_to_values
 from . import vmxml
 
-from .sampling import AdvancedStatsFunction, AdvancedStatsThread
+from .sampling import AdvancedStatsFunction, AdvancedStatsThread, SampleWindow
 from .utils import isVdsmImage
 from vmpowerdown import VmShutdown, VmReboot
 
@@ -636,9 +636,11 @@
                 if (sInfo and vmDrive.name in sInfo and
                         eInfo and vmDrive.name in eInfo):
                     # will be None if sampled during recovery
-                    dStats.update(_calcDiskRate(vmDrive, sInfo, eInfo,
-                                                sampleInterval))
-                    dStats.update(_calcDiskLatency(vmDrive, sInfo, eInfo))
+                    dStats.update(
+                        self._calcDiskRate(
+                            vmDrive, sInfo, eInfo, sampleInterval))
+                    dStats.update(
+                        self._calcDiskLatency(vmDrive, sInfo, eInfo))
 
             except (AttributeError, TypeError, ZeroDivisionError):
                 self._log.exception("Disk %s stats not available",
@@ -692,34 +694,396 @@
 
         return True
 
+    def _calcDiskRate(self, vmDrive, sInfo, eInfo, sampleInterval):
+        return {
+            'readRate': (
+                (eInfo[vmDrive.name]['rd_bytes'] -
+                 sInfo[vmDrive.name]['rd_bytes'])
+                / sampleInterval),
+            'writeRate': (
+                (eInfo[vmDrive.name]['wr_bytes'] -
+                 sInfo[vmDrive.name]['wr_bytes'])
+                / sampleInterval)}
 
-def _calcDiskRate(vmDrive, sInfo, eInfo, sampleInterval):
-    return {
-        'readRate': (
-            (eInfo[vmDrive.name]['rd_bytes'] -
-             sInfo[vmDrive.name]['rd_bytes'])
-            / sampleInterval),
-        'writeRate': (
-            (eInfo[vmDrive.name]['wr_bytes'] -
-             sInfo[vmDrive.name]['wr_bytes'])
-            / sampleInterval)}
+    def _calcDiskLatency(self, vmDrive, sInfo, eInfo):
+        dname = vmDrive.name
+
+        def compute_latency(ltype):
+            ops = ltype + '_operations'
+            operations = eInfo[dname][ops] - sInfo[dname][ops]
+            if not operations:
+                return 0
+            times = ltype + '_total_times'
+            elapsed_time = eInfo[dname][times] - sInfo[dname][times]
+            return elapsed_time / operations
+
+        return {'readLatency': str(compute_latency('rd')),
+                'writeLatency': str(compute_latency('wr')),
+                'flushLatency': str(compute_latency('flush'))}
 
 
-def _calcDiskLatency(vmDrive, sInfo, eInfo):
-    dname = vmDrive.name
+class VmStats(object):
+    MBPS_TO_BPS = 10 ** 6 / 8
 
-    def compute_latency(ltype):
-        ops = ltype + '_operations'
-        operations = eInfo[dname][ops] - sInfo[dname][ops]
-        if not operations:
-            return 0
-        times = ltype + '_total_times'
-        elapsed_time = eInfo[dname][times] - sInfo[dname][times]
-        return elapsed_time / operations
+    SAMPLE_NUM = 2
 
-    return {'readLatency': str(compute_latency('rd')),
-            'writeLatency': str(compute_latency('wr')),
-            'flushLatency': str(compute_latency('flush'))}
+    # This flag will prevent excessive log flooding when running
+    # on libvirt with no support for metadata xml elements.
+    #
+    # The issue currently exists only on CentOS/RHEL 6.5 that
+    # ships libvirt-0.10.x.
+    #
+    # TODO: Remove as soon as there is a hard dependency we can use
+    _libvirt_metadata_supported = True
+
+    def __init__(self, vm):
+        self._vm = vm
+        self._log = vm.log
+
+        self.last_sample_time = None
+
+        self.sampleBulk = SampleWindow(self.SAMPLE_NUM)
+
+        self._sampleVmJobs = SampleWindow(self.SAMPLE_NUM)
+        self._sampleCpuTune = SampleWindow(self.SAMPLE_NUM)
+
+    def sampleVmJobs(self):
+        # compatibility with VmStatsThread
+        jobs = self._vm.queryBlockJobs()
+        self._sampleVmJobs.append(jobs)
+        return jobs
+
+    def sampleCpuTune(self):
+        infos = self._vm._dom.schedulerParameters()
+        infos['vcpuCount'] = self._vm._dom.vcpusFlags(
+            libvirt.VIR_DOMAIN_VCPU_CURRENT)
+
+        metadataCpuLimit = None
+
+        try:
+            if VmStats._libvirt_metadata_supported:
+                metadataCpuLimit = self._vm._dom.metadata(
+                    libvirt.VIR_DOMAIN_METADATA_ELEMENT,
+                    METADATA_VM_TUNE_URI, 0)
+        except libvirt.libvirtError as e:
+            if e.get_error_code() == libvirt.VIR_ERR_ARGUMENT_UNSUPPORTED:
+                VmStats._libvirt_metadata_supported = False
+                self._log.error("libvirt does not support metadata")
+
+            elif (e.get_error_code()
+                  not in (libvirt.VIR_ERR_NO_DOMAIN,
+                          libvirt.VIR_ERR_NO_DOMAIN_METADATA)):
+                # Non-existing VM and no metadata block are expected
+                # conditions and no reasons for concern here.
+                # All other errors should be reported.
+                self._log.warn("Failed to retrieve QoS metadata because of %s",
+                               e)
+
+        if metadataCpuLimit:
+            metadataCpuLimitXML = _domParseStr(metadataCpuLimit)
+            nodeList = \
+                metadataCpuLimitXML.getElementsByTagName('vcpulimit')
+            infos['vcpuLimit'] = nodeList[0].childNodes[0].data
+
+        self._sampleCpuTune.append(infos)
+
+    def _getIoTuneStats(self, stats):
+        """
+        Collect the current ioTune settings for all disks VDSM knows about.
+
+        This assumes VDSM always has the correct info and nobody else is
+        touching the device without telling VDSM about it.
+
+        TODO: We might want to move to XML parsing (first update) and events
+        once libvirt supports them:
+        https://bugzilla.redhat.com/show_bug.cgi?id=1114492
+        """
+        ioTuneInfo = []
+
+        for disk in self._vm.getDiskDevices():
+            if "ioTune" in disk.specParams:
+                ioTuneInfo.append({
+                    "name": disk.name,
+                    "path": disk.path,
+                    "ioTune": disk.specParams["ioTune"]
+                })
+
+        stats['ioTune'] = ioTuneInfo
+
+    def _diff(self, prev, curr, val):
+        return prev[val] - curr[val]
+
+    def _usagePercentage(self, val, sampleInterval):
+        return 100 * val / sampleInterval / 1000 ** 3
+
+    def _getCpuStats(self, stats):
+        stats['cpuUser'] = 0.0
+        stats['cpuSys'] = 0.0
+
+        sInfo, eInfo, sampleInterval = self.sampleBulk.stats()
+
+        if sInfo is None:
+            return
+
+        try:
+            stats['cpuSys'] = self._usagePercentage(
+                self._diff(eInfo, sInfo, 'cpu.user') +
+                self._diff(eInfo, sInfo, 'cpu.system'),
+                sampleInterval)
+            stats['cpuUser'] = self._usagePercentage(
+                self._diff(eInfo, sInfo, 'cpu.time')
+                - self._diff(eInfo, sInfo, 'cpu.user')
+                - self._diff(eInfo, sInfo, 'cpu.system'),
+                sampleInterval)
+
+        except (KeyError, TypeError, ZeroDivisionError) as e:
+            self._log.exception("CPU stats not available: %s", e)
+
+    def _getBalloonStats(self, stats):
+        max_mem = int(self._vm.conf.get('memSize')) * 1024
+
+        sample = self.sampleBulk.last()
+
+        for dev in self._vm.getBalloonDevicesConf():
+            if dev['specParams']['model'] != 'none':
+                balloon_target = dev.get('target', max_mem)
+                break
+        else:
+            balloon_target = None
+
+        stats['balloonInfo'] = {}
+
+        # Do not return any balloon status info before we get all data
+        # MOM will ignore VMs with missing balloon information instead
+        # using incomplete data and computing wrong balloon targets
+        if balloon_target is not None and sample is not None:
+            stats['balloonInfo'].update({
+                'balloon_max': str(max_mem),
+                'balloon_min': str(
+                    int(self._vm.conf.get('memGuaranteedSize', '0')) * 1024),
+                'balloon_cur': str(sample['balloon.current']),
+                'balloon_target': str(balloon_target)
+            })
+
+    def _getCpuTuneInfo(self, stats):
+
+        sample = self._sampleCpuTune.last()
+
+        # Handling the case when not enough samples exist
+        if sample is None:
+            return
+
+        # Handling the case where quota is not set, setting to 0.
+        # According to libvirt API:"A quota with value 0 means no value."
+        # The value does not have to be present in some transient cases
+        if sample.get('vcpu_quota', _NO_CPU_QUOTA) != _NO_CPU_QUOTA:
+            stats['vcpuQuota'] = str(sample['vcpu_quota'])
+
+        # Handling the case where period is not set, setting to 0.
+        # According to libvirt API:"A period with value 0 means no value."
+        # The value does not have to be present in some transient cases
+        if sample.get('vcpu_period', _NO_CPU_PERIOD) != _NO_CPU_PERIOD:
+            stats['vcpuPeriod'] = sample['vcpu_period']
+
+    def _getCpuCount(self, stats):
+        sample = self._sampleCpuTune.last()
+
+        # Handling the case when not enough samples exist
+        if sample is None:
+            return
+
+        if 'vcpuCount' in sample:
+            vcpuCount = sample['vcpuCount']
+            if vcpuCount != -1:
+                stats['vcpuCount'] = vcpuCount
+            else:
+                self._log.error('Failed to get VM cpu count')
+
+    def _getUserCpuTuneInfo(self, stats):
+        sample = self._sampleCpuTune.last()
+
+        # Handling the case when not enough samples exist
+        if sample is None:
+            return
+
+        if 'vcpuLimit' in sample:
+            value = sample['vcpuLimit']
+            stats['vcpuUserLimit'] = value
+
+    @classmethod
+    def _getNicStats(cls, name, model, mac,
+                     start_sample, start_index,
+                     end_sample, end_index,
+                     interval):
+        ifSpeed = [100, 1000][model in ('e1000', 'virtio')]
+
+        ifStats = {'macAddr': mac,
+                   'name': name,
+                   'speed': str(ifSpeed),
+                   'state': 'unknown'}
+
+        ifStats['rxErrors'] = str(end_sample['net.%d.rx.errs' % end_index])
+        ifStats['rxDropped'] = str(end_sample['net.%d.rx.drop' % end_index])
+        ifStats['txErrors'] = str(end_sample['net.%d.tx.errs' % end_index])
+        ifStats['txDropped'] = str(end_sample['net.%d.tx.drop' % end_index])
+
+        rxDelta = (
+            end_sample['net.%d.rx.bytes' % end_index] -
+            start_sample['net.%d.rx.bytes' % start_index])
+        ifRxBytes = (100.0 *
+                     (rxDelta % 2 ** 32) /
+                     interval / ifSpeed / cls.MBPS_TO_BPS)
+        txDelta = (
+            end_sample['net.%d.tx.bytes' % end_index] -
+            start_sample['net.%d.tx.bytes' % start_index])
+        ifTxBytes = (100.0 *
+                     (txDelta % 2 ** 32) /
+                     interval / ifSpeed / cls.MBPS_TO_BPS)
+
+        ifStats['rxRate'] = '%.1f' % ifRxBytes
+        ifStats['txRate'] = '%.1f' % ifTxBytes
+
+        return ifStats
+
+    def _getNetworkStats(self, stats):
+        stats['network'] = {}
+        sInfo, eInfo, sampleInterval = self.sampleBulk.stats()
+
+        if sInfo is None:
+            return
+
+        for nic in self._vm.getNicDevices():
+            if nic.name.startswith('hostdev'):
+                continue
+
+            sIdx = _findBulkStatIndex('net', nic.name, sInfo)
+            eIdx = _findBulkStatIndex('net', nic.name, eInfo)
+            # may happen if nic is a new hot-plugged one
+            if sIdx is None or eIdx is None:
+                continue
+            stats['network'][nic.name] = self._getNicStats(
+                nic.name, nic.nicModel, nic.macAddr,
+                sInfo, sIdx, eInfo, eIdx, sampleInterval)
+
+    def _getDiskStats(self, stats):
+        sInfo, eInfo, sampleInterval = self.sampleBulk.stats()
+
+        for vmDrive in self._vm.getDiskDevices():
+            dStats = {}
+            try:
+                dStats = {'truesize': str(vmDrive.truesize),
+                          'apparentsize': str(vmDrive.apparentsize),
+                          'readLatency': '0',
+                          'writeLatency': '0',
+                          'flushLatency': '0'}
+                if isVdsmImage(vmDrive):
+                    dStats['imageID'] = vmDrive.imageID
+                elif "GUID" in vmDrive:
+                    dStats['lunGUID'] = vmDrive.GUID
+                sIdx = _findBulkStatIndex('block', vmDrive.name, sInfo)
+                eIdx = _findBulkStatIndex('block', vmDrive.name, eInfo)
+                if sIdx is not None and eIdx is not None:
+                    # will be None if sampled during recovery
+                    dStats.update(
+                        self._calcDiskRate(
+                            sInfo, sIdx, eInfo, eIdx, sampleInterval))
+                    dStats.update(
+                        self._calcDiskLatency(sInfo, sIdx, eInfo, eIdx))
+
+            except (KeyError, AttributeError, TypeError, ZeroDivisionError):
+                self._log.exception("Disk %s stats not available",
+                                    vmDrive.name)
+
+            stats[vmDrive.name] = dStats
+
+    def _getNumaStats(self, stats):
+        vmNumaNodeRuntimeMap = numaUtils.getVmNumaNodeRuntimeInfo(self._vm)
+        if vmNumaNodeRuntimeMap:
+            stats['vNodeRuntimeInfo'] = vmNumaNodeRuntimeMap
+
+    def _getVmJobs(self, stats):
+        info = self._sampleVmJobs.last()
+        if info is not None:
+            # If we are unable to collect stats we must not return anything at
+            # all since an empty dictionary would be interpreted as vm jobs
+            # finishing.
+            stats['vmJobs'] = info
+
+    def get(self):
+        stats = {}
+
+        try:
+            stats['statsAge'] = time.time() - self.last_sample_time
+        except TypeError:
+            self._log.debug("Stats age not available")
+            stats['statsAge'] = -1.0
+
+        self._getCpuStats(stats)
+        self._getNetworkStats(stats)
+        self._getDiskStats(stats)
+        self._getBalloonStats(stats)
+        self._getVmJobs(stats)
+        self._getNumaStats(stats)
+        self._getCpuTuneInfo(stats)
+        self._getCpuCount(stats)
+        self._getUserCpuTuneInfo(stats)
+        self._getIoTuneStats(stats)
+
+        return stats
+
+    # compatibility with VmStatsThread
+
+    def start(self):
+        pass
+
+    def stop(self):
+        pass
+
+    def pause(self):
+        pass
+
+    def cont(self):
+        pass
+
+    def getLastSampleTime(self):
+        return self.last_sample_time
+
+    def _calcDiskRate(self, sInfo, sIdx, eInfo, eIdx, sampleInterval):
+        return {
+            'readRate': (
+                (eInfo['block.%d.rd.bytes' % eIdx] -
+                 sInfo['block.%d.rd.bytes' % sIdx])
+                / sampleInterval),
+            'writeRate': (
+                (eInfo['block.%d.wr.bytes' % eIdx] -
+                 sInfo['block.%d.wr.bytes' % sIdx])
+                / sampleInterval)}
+
+    def _calcDiskLatency(self, sInfo, sIdx, eInfo, eIdx):
+        def compute_latency(ltype):
+            ops = ltype + '.reqs'
+            operations = (
+                eInfo['block.%d.%s' % (eIdx, ops)] -
+                sInfo['block.%d.%s' % (sIdx, ops)])
+            if not operations:
+                return 0
+            times = ltype + '.times'
+            elapsed_time = (
+                eInfo['block.%d.%s' % (eIdx, times)] -
+                sInfo['block.%d.%s' % (sIdx, times)])
+            return elapsed_time / operations
+
+        return {'readLatency': str(compute_latency('rd')),
+                'writeLatency': str(compute_latency('wr')),
+                'flushLatency': str(compute_latency('fl'))}
+
+
+def _findBulkStatIndex(group, value, stats, attr='name'):
+    if stats:
+        for idx in xrange(stats['%s.count' % group]):
+            if stats['%s.%d.%s' % (group, idx, attr)] == value:
+                return idx
+    return None
 
 
 class TimeoutError(libvirt.libvirtError):
@@ -2562,7 +2926,10 @@
         return domxml.toxml()
 
     def _initVmStats(self):
-        self.stats = VmStatsThread(self)
+        if config.getboolean('vars', 'vm_sample_bulk'):
+            self.stats = VmStats(self)
+        else:
+            self.stats = VmStatsThread(self)
         self.stats.start()
         self._guestEventTime = self._startTime
 


-- 
To view, visit http://gerrit.ovirt.org/36722
To unsubscribe, visit http://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ia2f1a62515059e663b418c6c7b61a15881980dc9
Gerrit-PatchSet: 1
Gerrit-Project: vdsm
Gerrit-Branch: master
Gerrit-Owner: Francesco Romani <fromani at redhat.com>


More information about the vdsm-patches mailing list