mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-05-12 08:36:53 +02:00
[ui] ChunksMonitor: asynchronous status files modification time polling
* make last modification time check on status asynchronous using a dedicated thread + use a ThreadPool to run tasks in parallel * avoid UI freeze when checking for status updates + increase performances
This commit is contained in:
parent
55dba55d19
commit
e40b0e57b8
2 changed files with 108 additions and 28 deletions
|
@ -101,8 +101,9 @@ class MeshroomApp(QApplication):
|
||||||
# additional context properties
|
# additional context properties
|
||||||
self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self))
|
self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self))
|
||||||
self.engine.rootContext().setContextProperty("MeshroomApp", self)
|
self.engine.rootContext().setContextProperty("MeshroomApp", self)
|
||||||
# Request any potential computation to stop on exit
|
|
||||||
self.aboutToQuit.connect(r.stopExecution)
|
# request any potential computation to stop on exit
|
||||||
|
self.aboutToQuit.connect(r.stopChildThreads)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.')
|
parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.')
|
||||||
parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False,
|
parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False,
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
# coding:utf-8
|
# coding:utf-8
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from threading import Thread
|
from threading import Thread, Event
|
||||||
|
from multiprocessing.pool import ThreadPool
|
||||||
|
|
||||||
from PySide2.QtCore import Slot, QJsonValue, QObject, QUrl, Property, Signal, QPoint
|
from PySide2.QtCore import Slot, QJsonValue, QObject, QUrl, Property, Signal, QPoint
|
||||||
|
|
||||||
|
@ -16,6 +18,61 @@ from meshroom.ui import commands
|
||||||
from meshroom.ui.utils import makeProperty
|
from meshroom.ui.utils import makeProperty
|
||||||
|
|
||||||
|
|
||||||
|
class FilesModTimePollerThread(QObject):
|
||||||
|
"""
|
||||||
|
Thread responsible for non-blocking polling of last modification times of a list of files.
|
||||||
|
Uses a Python ThreadPool internally to split tasks on multiple threads.
|
||||||
|
"""
|
||||||
|
timesAvailable = Signal(list)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(FilesModTimePollerThread, self).__init__(parent)
|
||||||
|
self._thread = None
|
||||||
|
self._threadPool = ThreadPool(4)
|
||||||
|
self._stopFlag = Event()
|
||||||
|
self._refreshInterval = 5 # refresh interval in seconds
|
||||||
|
self._files = []
|
||||||
|
|
||||||
|
def start(self, files):
|
||||||
|
""" Start polling thread.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files: the list of files to monitor
|
||||||
|
"""
|
||||||
|
if self._thread:
|
||||||
|
# thread already running, return
|
||||||
|
return
|
||||||
|
if not files:
|
||||||
|
# file list is empty
|
||||||
|
return
|
||||||
|
self._stopFlag.clear()
|
||||||
|
self._files = files or []
|
||||||
|
self._thread = Thread(target=self.run)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
""" Request polling thread to stop. """
|
||||||
|
if not self._thread:
|
||||||
|
return
|
||||||
|
self._stopFlag.set()
|
||||||
|
self._thread.join()
|
||||||
|
self._thread = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getFileLastModTime(f):
|
||||||
|
""" Return 'mtime' of the file if it exists, -1 otherwise. """
|
||||||
|
try:
|
||||||
|
return os.path.getmtime(f)
|
||||||
|
except OSError:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
""" Poll watched files for last modification time. """
|
||||||
|
while not self._stopFlag.wait(self._refreshInterval):
|
||||||
|
times = self._threadPool.map(FilesModTimePollerThread.getFileLastModTime, self._files)
|
||||||
|
self.timesAvailable.emit(times)
|
||||||
|
|
||||||
|
|
||||||
class ChunksMonitor(QObject):
|
class ChunksMonitor(QObject):
|
||||||
"""
|
"""
|
||||||
ChunksMonitor regularly check NodeChunks' status files for modification and trigger their update on change.
|
ChunksMonitor regularly check NodeChunks' status files for modification and trigger their update on change.
|
||||||
|
@ -29,52 +86,69 @@ class ChunksMonitor(QObject):
|
||||||
def __init__(self, chunks=(), parent=None):
|
def __init__(self, chunks=(), parent=None):
|
||||||
super(ChunksMonitor, self).__init__(parent)
|
super(ChunksMonitor, self).__init__(parent)
|
||||||
self.lastModificationRecords = dict()
|
self.lastModificationRecords = dict()
|
||||||
|
self._filesTimePoller = FilesModTimePollerThread(parent=self)
|
||||||
|
self._filesTimePoller.timesAvailable.connect(self.compareFilesTimes)
|
||||||
|
self._pollerOutdated = False
|
||||||
self.setChunks(chunks)
|
self.setChunks(chunks)
|
||||||
# Check status files every x seconds
|
|
||||||
# TODO: adapt frequency according to graph compute status
|
|
||||||
self.startTimer(5000)
|
|
||||||
|
|
||||||
def setChunks(self, chunks):
|
def setChunks(self, chunks):
|
||||||
""" Set the list of chunks to monitor. """
|
""" Set the list of chunks to monitor. """
|
||||||
|
self._filesTimePoller.stop()
|
||||||
self.clear()
|
self.clear()
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
f = chunk.statusFile
|
# initialize last modification times to current time for all chunks
|
||||||
# Store a record of {chunk: status file last modification}
|
self.lastModificationRecords[chunk] = time.time()
|
||||||
self.lastModificationRecords[chunk] = self.getFileLastModTime(f)
|
|
||||||
# For local use, handle statusChanged emitted directly from the node chunk
|
# For local use, handle statusChanged emitted directly from the node chunk
|
||||||
chunk.statusChanged.connect(self.onChunkStatusChanged)
|
chunk.statusChanged.connect(self.onChunkStatusChanged)
|
||||||
|
self._pollerOutdated = True
|
||||||
self.chunkStatusChanged.emit(None, -1)
|
self.chunkStatusChanged.emit(None, -1)
|
||||||
|
self._filesTimePoller.start(self.statusFiles)
|
||||||
|
self._pollerOutdated = False
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
""" Stop the status files monitoring. """
|
||||||
|
self._filesTimePoller.stop()
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
""" Clear the list of monitored chunks """
|
""" Clear the list of monitored chunks. """
|
||||||
for ch in self.lastModificationRecords:
|
for chunk in self.lastModificationRecords:
|
||||||
ch.statusChanged.disconnect(self.onChunkStatusChanged)
|
chunk.statusChanged.disconnect(self.onChunkStatusChanged)
|
||||||
self.lastModificationRecords.clear()
|
self.lastModificationRecords.clear()
|
||||||
|
|
||||||
def timerEvent(self, evt):
|
|
||||||
self.checkFileTimes()
|
|
||||||
|
|
||||||
def onChunkStatusChanged(self):
|
def onChunkStatusChanged(self):
|
||||||
""" React to change of status coming from the NodeChunk itself. """
|
""" React to change of status coming from the NodeChunk itself. """
|
||||||
chunk = self.sender()
|
chunk = self.sender()
|
||||||
assert chunk in self.lastModificationRecords
|
assert chunk in self.lastModificationRecords
|
||||||
# Update record entry for this file so that it's up-to-date on next timerEvent
|
# update record entry for this file so that it's up-to-date on next timerEvent
|
||||||
self.lastModificationRecords[chunk] = self.getFileLastModTime(chunk.statusFile)
|
# use current time instead of actual file's mtime to limit filesystem requests
|
||||||
|
self.lastModificationRecords[chunk] = time.time()
|
||||||
self.chunkStatusChanged.emit(chunk, chunk.status.status)
|
self.chunkStatusChanged.emit(chunk, chunk.status.status)
|
||||||
|
|
||||||
@staticmethod
|
@property
|
||||||
def getFileLastModTime(f):
|
def statusFiles(self):
|
||||||
""" Return 'mtime' of the file if it exists, -1 otherwise. """
|
""" Get status file paths from current chunks. """
|
||||||
return os.path.getmtime(f) if os.path.exists(f) else -1
|
return [c.statusFile for c in self.lastModificationRecords.keys()]
|
||||||
|
|
||||||
def checkFileTimes(self):
|
def compareFilesTimes(self, times):
|
||||||
""" Check status files last modification time and compare with stored value """
|
"""
|
||||||
for chunk, t in self.lastModificationRecords.items():
|
Compare previous file modification times with results from last poll.
|
||||||
lastMod = self.getFileLastModTime(chunk.statusFile)
|
Trigger chunk status update if file was modified since.
|
||||||
if lastMod != t:
|
|
||||||
self.lastModificationRecords[chunk] = lastMod
|
Args:
|
||||||
|
times: the last modification times for currently monitored files.
|
||||||
|
"""
|
||||||
|
if self._pollerOutdated:
|
||||||
|
return
|
||||||
|
|
||||||
|
newRecords = dict(zip(self.lastModificationRecords.keys(), times))
|
||||||
|
for chunk, previousTime in self.lastModificationRecords.items():
|
||||||
|
lastModTime = newRecords.get(chunk, -1)
|
||||||
|
# update chunk status if:
|
||||||
|
# - last modification time is more recent than previous record
|
||||||
|
# - file is no more available (-1)
|
||||||
|
if lastModTime > previousTime or (lastModTime == -1 != previousTime):
|
||||||
|
self.lastModificationRecords[chunk] = lastModTime
|
||||||
chunk.updateStatusFromCache()
|
chunk.updateStatusFromCache()
|
||||||
logging.debug("Status for node {} changed: {}".format(chunk.node, chunk.status.status))
|
|
||||||
|
|
||||||
chunkStatusChanged = Signal(NodeChunk, int)
|
chunkStatusChanged = Signal(NodeChunk, int)
|
||||||
|
|
||||||
|
@ -244,6 +318,11 @@ class UIGraph(QObject):
|
||||||
self._sortedDFSChunks.clear()
|
self._sortedDFSChunks.clear()
|
||||||
self._undoStack.clear()
|
self._undoStack.clear()
|
||||||
|
|
||||||
|
def stopChildThreads(self):
|
||||||
|
""" Stop all child threads. """
|
||||||
|
self.stopExecution()
|
||||||
|
self._chunksMonitor.stop()
|
||||||
|
|
||||||
def load(self, filepath):
|
def load(self, filepath):
|
||||||
g = Graph('')
|
g = Graph('')
|
||||||
g.load(filepath)
|
g.load(filepath)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue