Starting latex render locking.

This commit is contained in:
Adsooi 2024-10-15 19:21:40 +02:00
parent ccddb068a6
commit aeaaba759f
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
4 changed files with 99 additions and 17 deletions

View file

@ -529,11 +529,15 @@ class CanvasAPI extends Module {
this.#canvas.loadImageAsync(imgData.source).then(() => { this.#canvas.loadImageAsync(imgData.source).then(() => {
if(this.#redrawCount === currentRedrawCount) if(this.#redrawCount === currentRedrawCount)
callback(imgData) callback(imgData)
else
console.log("2Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
}) })
} else { } else {
// Callback directly // Callback directly
if(this.#redrawCount === currentRedrawCount) if(this.#redrawCount === currentRedrawCount)
callback(imgData) callback(imgData)
else
console.log("2Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
} }
} }
const prerendered = Latex.findPrerendered(ltxText, this.textsize, color) const prerendered = Latex.findPrerendered(ltxText, this.textsize, color)

Binary file not shown.

View file

@ -82,6 +82,7 @@ class Latex(QObject):
def __init__(self, cache_path): def __init__(self, cache_path):
QObject.__init__(self) QObject.__init__(self)
self.tempdir = path.join(cache_path, "latex") self.tempdir = path.join(cache_path, "latex")
self.render_pipeline_locks = {}
makedirs(self.tempdir, exist_ok=True) makedirs(self.tempdir, exist_ok=True)
@Property(bool) @Property(bool)
@ -116,20 +117,71 @@ class Latex(QObject):
except MissingPackageException: except MissingPackageException:
valid_install = False # Should have sent an error message if failed to render valid_install = False # Should have sent an error message if failed to render
return valid_install return valid_install
def lock(self, markup_hash, render_hash, promise):
"""
Locks the render pipeline for a given markup hash and render hash.
"""
# print("Locking", markup_hash, render_hash)
if markup_hash not in self.render_pipeline_locks:
self.render_pipeline_locks[markup_hash] = promise
self.render_pipeline_locks[render_hash] = promise
def release_lock(self, markup_hash, render_hash):
"""
Release locks on the markup and render hashes.
"""
# print("Releasing", markup_hash, render_hash)
if markup_hash in self.render_pipeline_locks:
del self.render_pipeline_locks[markup_hash]
del self.render_pipeline_locks[render_hash]
@Slot(str, float, QColor, result=PyPromise) @Slot(str, float, QColor, result=PyPromise)
def renderAsync(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise: def renderAsync(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise:
""" """
Prepares and renders a latex string into a png file asynchronously. Prepares and renders a latex string into a png file asynchronously.
""" """
return PyPromise(self.renderSync, [latex_markup, font_size, color]) markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
promise = None
if render_hash in self.render_pipeline_locks:
# A PyPromise for this specific render is already running.
# print("Already running render of", latex_markup)
promise = self.render_pipeline_locks[render_hash]
elif markup_hash in self.render_pipeline_locks:
# A PyPromise with the same markup, but not the same color or font size is already running.
print("Chaining render of", latex_markup)
existing_promise = self.render_pipeline_locks[markup_hash]
promise = self._create_async_promise(latex_markup, font_size, color)
existing_promise.then(lambda x, latex_markup=latex_markup: print("> Starting chained render of", latex_markup))
promise.then(lambda x, latex_markup=latex_markup: print("> Fulfilled chained render of", latex_markup, "\n with", x.toVariant()))
existing_promise.then(promise.start)
else:
# No such PyPromise is running.
promise = self._create_async_promise(latex_markup, font_size, color)
promise.start()
return promise
def _create_async_promise(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise:
"""
Createsa PyPromise to render a latex string into a PNG file.
Internal method. Use renderAsync that makes use of locks.
"""
markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
promise = PyPromise(self.renderSync, [latex_markup, font_size, color], start_automatically=False)
self.lock(markup_hash, render_hash, promise)
# Make the lock release at the end.
def unlock(data, markup_hash=markup_hash, render_hash=render_hash):
self.release_lock(markup_hash, render_hash)
promise.then(unlock, unlock)
return promise
@Slot(str, float, QColor, result=str) @Slot(str, float, QColor, result=str)
def renderSync(self, latex_markup: str, font_size: float, color: QColor) -> str: def renderSync(self, latex_markup: str, font_size: float, color: QColor) -> str:
""" """
Prepares and renders a latex string into a png file. Prepares and renders a latex string into a png file.
""" """
markup_hash, export_path = self.create_export_path(latex_markup, font_size, color) markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
if self.latexSupported and not path.exists(export_path + ".png"): if self.latexSupported and not path.exists(export_path + ".png"):
print("Rendering", latex_markup, export_path) print("Rendering", latex_markup, export_path)
# Generating file # Generating file
@ -138,14 +190,12 @@ class Latex(QObject):
if not path.exists(latex_path + ".dvi"): if not path.exists(latex_path + ".dvi"):
self.create_latex_doc(latex_path, latex_markup) self.create_latex_doc(latex_path, latex_markup)
self.convert_latex_to_dvi(latex_path) self.convert_latex_to_dvi(latex_path)
# self.cleanup(latex_path) self.cleanup(latex_path)
# Creating four pictures of different sizes to better handle dpi. # Creating four pictures of different sizes to better handle dpi.
self.convert_dvi_to_png(latex_path, export_path, font_size, color) self.convert_dvi_to_png(latex_path, export_path, font_size, color)
# self.convert_dvi_to_png(latex_path, export_path+"@2", font_size*2, color) # self.convert_dvi_to_png(latex_path, export_path+"@2", font_size*2, color)
# self.convert_dvi_to_png(latex_path, export_path+"@3", font_size*3, color) # self.convert_dvi_to_png(latex_path, export_path+"@3", font_size*3, color)
# self.convert_dvi_to_png(latex_path, export_path+"@4", font_size*4, color) # self.convert_dvi_to_png(latex_path, export_path+"@4", font_size*4, color)
else:
sleep(0)
img = QImage(export_path) img = QImage(export_path)
# Small hack, not very optimized since we load the image twice, but you can't pass a QImage to QML and expect it to be loaded # Small hack, not very optimized since we load the image twice, but you can't pass a QImage to QML and expect it to be loaded
return f'{export_path}.png,{img.width()},{img.height()}' return f'{export_path}.png,{img.width()},{img.height()}'
@ -155,7 +205,7 @@ class Latex(QObject):
""" """
Finds a prerendered image and returns its data if possible, and an empty string if not. Finds a prerendered image and returns its data if possible, and an empty string if not.
""" """
markup_hash, export_path = self.create_export_path(latex_markup, font_size, color) markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color)
data = "" data = ""
if path.exists(export_path + ".png"): if path.exists(export_path + ".png"):
img = QImage(export_path) img = QImage(export_path)
@ -165,10 +215,13 @@ class Latex(QObject):
def create_export_path(self, latex_markup: str, font_size: float, color: QColor): def create_export_path(self, latex_markup: str, font_size: float, color: QColor):
""" """
Standardizes export path for renders. Standardizes export path for renders.
Markup hash is unique for the markup
Render hash is unique for the markup, the font size and the color.
""" """
markup_hash = "render" + str(sha512(latex_markup.encode()).hexdigest()) markup_hash = "render" + str(sha512(latex_markup.encode()).hexdigest())
export_path = path.join(self.tempdir, f'{markup_hash}_{int(font_size)}_{color.rgb()}') render_hash = f'{markup_hash}_{int(font_size)}_{color.rgb()}'
return markup_hash, export_path export_path = path.join(self.tempdir, render_hash)
return markup_hash, render_hash, export_path
def create_latex_doc(self, export_path: str, latex_markup: str): def create_latex_doc(self, export_path: str, latex_markup: str):
""" """

View file

@ -23,6 +23,18 @@ from PySide6.QtQml import QJSValue
from LogarithmPlotter.util.js import PyJSValue from LogarithmPlotter.util.js import PyJSValue
def check_callable(function: Callable|QJSValue) -> Callable|None:
"""
Checks if the given function can be called (either a python callable
or a QJSValue function), and returns the object that can be called directly.
Returns None if not a function.
"""
if isinstance(function, QJSValue) and function.isCallable():
return PyJSValue(function)
elif isinstance(function, Callable):
return function
return None
class InvalidReturnValue(Exception): pass class InvalidReturnValue(Exception): pass
@ -69,16 +81,30 @@ class PyPromise(QObject):
fulfilled = Signal((QJSValue,), (QObject,)) fulfilled = Signal((QJSValue,), (QObject,))
rejected = Signal(Exception) rejected = Signal(Exception)
def __init__(self, to_run: Callable, args=[]): def __init__(self, to_run: Callable|QJSValue, args=[], start_automatically=True):
QObject.__init__(self) QObject.__init__(self)
self._fulfills = [] self._fulfills = []
self._rejects = [] self._rejects = []
self._state = "pending" self._state = "pending"
self._started = False
self.fulfilled.connect(self._fulfill) self.fulfilled.connect(self._fulfill)
self.rejected.connect(self._reject) self.rejected.connect(self._reject)
to_run = check_callable(to_run)
if to_run is None:
raise ValueError("New PyPromise created with invalid function")
self._runner = PyPromiseRunner(to_run, self, args) self._runner = PyPromiseRunner(to_run, self, args)
QThreadPool.globalInstance().start(self._runner) if start_automatically:
self._start()
@Slot()
def start(self, *args, **kwargs):
"""
Starts the thread that will run the promise.
"""
if not self._started: # Avoid getting started twice.
QThreadPool.globalInstance().start(self._runner)
self._started = True
@Property(str) @Property(str)
def state(self): def state(self):
return self._state return self._state
@ -89,13 +115,11 @@ class PyPromise(QObject):
""" """
Adds listeners for both fulfilment and catching errors of the Promise. Adds listeners for both fulfilment and catching errors of the Promise.
""" """
if isinstance(on_fulfill, QJSValue): on_fulfill = check_callable(on_fulfill)
self._fulfills.append(PyJSValue(on_fulfill)) on_reject = check_callable(on_reject)
elif isinstance(on_fulfill, Callable): if on_fulfill is not None:
self._fulfills.append(on_fulfill) self._fulfills.append(on_fulfill)
if isinstance(on_reject, QJSValue): if on_reject is not None:
self._rejects.append(PyJSValue(on_reject))
elif isinstance(on_reject, Callable):
self._rejects.append(on_reject) self._rejects.append(on_reject)
return self return self
@ -107,6 +131,7 @@ class PyPromise(QObject):
for on_fulfill in self._fulfills: for on_fulfill in self._fulfills:
try: try:
result = on_fulfill(data) result = on_fulfill(data)
result = result.qjs_value if isinstance(result, PyJSValue) else result
data = result if result not in no_return else data # Forward data. data = result if result not in no_return else data # Forward data.
except Exception as e: except Exception as e:
self._reject(repr(e)) self._reject(repr(e))