Source code for qgis_profiler.meters.map_rendering

#  Copyright (c) 2025-2026 profiler-qgis-plugin contributors.
#
#
#  This file is part of profiler-qgis-plugin.
#
#  profiler-qgis-plugin is free software: you can redistribute it and/or
#  modify it under the terms of the GNU General Public License as published
#  by the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  profiler-qgis-plugin is distributed in the hope that it will be
#  useful, but WITHOUT ANY WARRANTY; without even the implied warranty
#  of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with profiler-qgis-plugin. If not, see <https://www.gnu.org/licenses/>.

"""Meter that monitors map canvas rendering times.

Contains :class:`MapRenderingMeter`, which listens to map canvas render signals
and reports an anomaly when rendering exceeds a configurable time threshold.
"""

import logging
import time
from contextlib import suppress
from typing import TYPE_CHECKING, Optional, cast

from qgis.core import QgsApplication
from qgis.PyQt.QtCore import QElapsedTimer
from qgis.utils import iface as iface_

from qgis_profiler.meters.meter import Meter
from qgis_profiler.settings import Settings

if TYPE_CHECKING:
    from qgis.gui import QgisInterface

LOGGER = logging.getLogger(__name__)

iface = cast("QgisInterface", iface_)


[docs] class MapRenderingMeter(Meter): """Measures the time it takes to fully render the map.""" _short_name = "rendering" _instance: Optional["MapRenderingMeter"] = None def __init__( self, threshold_s: float, ) -> None: """Initialize with a rendering time threshold in seconds.""" super().__init__(supports_continuous_measurement=True) self._threshold_ms = threshold_s * 1000 self._elapsed_timer = QElapsedTimer() self._last_rendering_time_ms: int = 0 LOGGER.debug("MapRenderingMeasurer parameters initialized: %s", self) def __str__(self) -> str: """Return a string representation of the meter parameters.""" return f"MapRenderingMeasurer(threshold_s={self._threshold_ms / 1000}),"
[docs] @classmethod def get(cls) -> "MapRenderingMeter": """Return the singleton MapRenderingMeter instance.""" if cls._instance is None: cls._instance = MapRenderingMeter( threshold_s=Settings.map_rendering_meter_threshold.get(), ) cls._instance.enabled = Settings.map_rendering_meter_enabled.get() return cls._instance
[docs] def reset_parameters(self) -> None: """Reset measurement parameters from current settings.""" self._threshold_ms = Settings.map_rendering_meter_threshold.get() * 1000 self.enabled = Settings.map_rendering_meter_enabled.get() LOGGER.debug("MapRenderingMeasurer parameters reset: %s", self)
def _start_measuring(self) -> bool: LOGGER.debug("Starting map rendering measuring") iface.mapCanvas().renderStarting.connect(self._rendering_started) iface.mapCanvas().mapCanvasRefreshed.connect(self._rendering_finished) return True def _stop_measuring(self) -> None: with suppress(TypeError): iface.mapCanvas().renderStarting.disconnect(self._rendering_started) with suppress(TypeError): iface.mapCanvas().renderStarting.disconnect(self._rendering_started) def _rendering_started(self) -> None: self._elapsed_timer.restart() def _rendering_finished(self) -> None: elapsed_ms = self._elapsed_timer.elapsed() if elapsed_ms > self._threshold_ms: LOGGER.debug("Map rendering time exceeded threshold %s ms", elapsed_ms) self._emit_anomaly(round(elapsed_ms / 1000, 3)) self._last_rendering_time_ms = elapsed_ms def _measure(self) -> tuple[float, bool]: if self.is_measuring: return ( self._last_rendering_time_ms / 1000, self._last_rendering_time_ms > self._threshold_ms, ) self._last_rendering_time_ms = 0 self.start_measuring() iface.mapCanvas().redrawAllLayers() t = time.time() timeout = 10 while self._last_rendering_time_ms == 0 and time.time() - t < timeout: QgsApplication.processEvents() rendering_time = (self._last_rendering_time_ms / 1000) or timeout return rendering_time, rendering_time > self._threshold_ms * 1000