Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimizations #48

Merged
merged 11 commits into from
May 31, 2021
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.8
1.0.9
25 changes: 20 additions & 5 deletions bip/t3dn_bip/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
class _BIPFormat:
'''BIP format info.'''

def __init__(self, magic: bytes):
def __init__(self, exts: list, magic: bytes):
self.exts = exts
self.magic = magic


class _PILFormat:
'''PIL format info.'''

def __init__(self, magic: bytes, tests: list):
def __init__(self, exts: list, magic: bytes, tests: list):
self.exts = exts
self.magic = magic
self.tests = tests
self.supported = False
Expand All @@ -27,12 +29,25 @@ def __init__(self, magic: bytes, tests: list):
]

BIP_FORMATS = {
'BIP2': _BIPFormat(magic=b'BIP2'),
'BIP2': _BIPFormat(
exts=['.bip', '.bip2'],
magic=b'BIP2',
),
}

PIL_FORMATS = {
'PNG': _PILFormat(magic=b'\x89\x50\x4e\x47', tests=_png_tests),
'JPG': _PILFormat(magic=b'\xff\xd8', tests=_jpg_tests),
'PNG':
_PILFormat(
exts=['.png'],
magic=b'\x89\x50\x4e\x47',
tests=_png_tests,
),
'JPG':
_PILFormat(
exts=['.jpg', '.jpeg', '.jpe', '.jif', '.jfif'],
magic=b'\xff\xd8',
tests=_jpg_tests,
),
}

MAGIC_LENGTH = max(
Expand Down
105 changes: 23 additions & 82 deletions bip/t3dn_bip/previews.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import bpy
import bpy.utils.previews
from bpy.types import ImagePreview
from multiprocessing.dummy import Pool
from multiprocessing import cpu_count
from threading import Event
from queue import Queue
from traceback import print_exc
from time import time
from typing import ItemsView, Iterator, KeysView, ValuesView
from .utils import support_pillow, can_load, load_file, tag_redraw
from .utils import support_pillow, can_load, load_file
from .formats import unsupported_formats

WARNINGS = True
from .threads import load_async
from . import settings


class ImagePreviewCollection:
'''Dictionary-like class of previews.'''

def __init__(self, max_size: tuple = (128, 128), lazy_load: bool = True):
'''Create collection and start internal timer.'''
if WARNINGS:
if settings.WARNINGS:
if not support_pillow():
print('Pillow is not installed, therefore:')
print('- BIP images load without scaling.')
Expand Down Expand Up @@ -47,12 +42,7 @@ def __init__(self, max_size: tuple = (128, 128), lazy_load: bool = True):
self._lazy_load = lazy_load

if self._lazy_load:
self._pool = Pool(processes=cpu_count())
self._event = None
self._queue = Queue()

if not bpy.app.timers.is_registered(self._timer):
bpy.app.timers.register(self._timer, persistent=True)
self._abort_signal = None

def __len__(self) -> int:
'''Return the amount of previews in the collection.'''
Expand Down Expand Up @@ -123,10 +113,12 @@ def load(self, name: str, filepath: str, filetype: str) -> ImagePreview:

preview = self.new(name)

self._pool.apply_async(
func=self._load_async,
args=(name, filepath, self._get_event()),
error_callback=print,
load_async(
self._collection,
name,
filepath,
self._max_size,
self._get_abort_signal(),
)

return preview
Expand Down Expand Up @@ -158,83 +150,32 @@ def _load_eager(self, name: str, filepath: str) -> ImagePreview:

return preview

def _load_async(self, name: str, filepath: str, event: Event):
'''Load image contents from file and queue preview load.'''
if not event.is_set():
data = load_file(filepath, self._max_size)

if not event.is_set():
self._queue.put((name, data, event))

def _timer(self):
'''Load queued image contents into previews.'''
now = time()
redraw = False
delay = 0.1

while time() - now < 0.1:
try:
args = self._queue.get(block=False)
except:
break

try:
self._load_queued(*args)
except:
print_exc()
else:
redraw = True

else:
delay = 0.0

if redraw:
tag_redraw()

return delay

def _load_queued(self, name: str, data: dict, event: Event):
'''Load queued image contents into preview.'''
if not event.is_set():
if name in self:
preview = self[name]
preview.icon_size = data['icon_size']
preview.icon_pixels = data['icon_pixels']
preview.image_size = data['image_size']
preview.image_pixels = data['image_pixels']

def clear(self):
'''Clear all previews.'''
if self._lazy_load:
self._set_event()

with self._queue.mutex:
self._queue.queue.clear()
self._set_abort_signal()

self._collection.clear()

def close(self):
'''Close the collection and clear all previews.'''
if self._lazy_load:
self._set_event()

if bpy.app.timers.is_registered(self._timer):
bpy.app.timers.unregister(self._timer)
self._set_abort_signal()

self._collection.close()

def _get_event(self) -> Event:
'''Get the clear event, make one if necesssary.'''
if self._event is None:
self._event = Event()
def _get_abort_signal(self) -> Event:
'''Get the abort signal, make one if necesssary.'''
if self._abort_signal is None:
self._abort_signal = Event()

return self._event
return self._abort_signal

def _set_event(self):
'''Set the clear event, then remove the reference.'''
if self._event is not None:
self._event.set()
self._event = None
def _set_abort_signal(self):
'''Set the abort signal, then remove the reference.'''
if self._abort_signal is not None:
self._abort_signal.set()
self._abort_signal = None


def new(
Expand Down
8 changes: 8 additions & 0 deletions bip/t3dn_bip/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Print warnings about which features are supported.
WARNINGS = True

# Use magic bytes to check file type, instead of extension.
USE_MAGIC = False

# Max number of threads used for loading image contents.
MAX_THREADS = 4
124 changes: 124 additions & 0 deletions bip/t3dn_bip/threads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import bpy
import bpy.utils.previews
from queue import Queue
from threading import Thread, Event
from time import time
from traceback import print_exc
from multiprocessing import cpu_count
from .utils import load_file, tag_redraw
from . import settings

_pending = 0
_queue_read = Queue()
_queue_emplace = Queue()
_thread_stop_signal = None


def _read_thread(stop_signal: Event):
'''Read image data in the background.'''
# Run read loop until we are stopped.
while not stop_signal.is_set():
# Try to get the next item from the read queue. Wait up to 1 second.
try:
results = _queue_read.get(block=True, timeout=1.0)
collection, name, filepath, max_size, abort_signal = results
except:
continue

# Try to load image.
data = None
if not abort_signal.is_set():
try:
data = load_file(filepath, max_size)
except:
print_exc()

# Queue for emplacement.
_queue_emplace.put((collection, name, data, abort_signal))


def _emplace_timer():
'''Emplaces pixels into the preview object. Runs on the main thread.'''
global _pending
global _thread_stop_signal

# Variables for timer batch management.
now = time()
delay = 0.1
redraw = False

# Take around 100ms for this batch.
while time() - now < 0.1:
# Get the next item from the emplace queue.
try:
results = _queue_emplace.get(block=False)
collection, name, data, abort_signal = results
except:
break

# Decrement images that need to be loaded.
_pending -= 1

# Move data to preview object.
if not abort_signal.is_set() and name in collection:
try:
preview = collection[name]
preview.icon_size = data['icon_size']
preview.icon_pixels = data['icon_pixels']
preview.image_size = data['image_size']
preview.image_pixels = data['image_pixels']
except:
print_exc()
else:
redraw = True

# There might be more in the queue. Let's get scheduled soon.
else:
delay = 0.01

# Redraw UI in case we updated preview objects.
if redraw:
tag_redraw()

# If no items are pending, stop read thread and emplace timer.
if not _pending:
# Stop read thread.
if _thread_stop_signal:
_thread_stop_signal.set()
_thread_stop_signal = None

# Don't schedule emplace timer.
delay = None

# Schedule next timer call.
return delay


def load_async(
collection: bpy.utils.previews.ImagePreviewCollection,
name: str,
filepath: str,
max_size: tuple,
abort_signal: Event,
):
'''Load image asynchronously. Needs to be called on the main thread.'''
global _pending
global _thread_stop_signal

# Increment images that need to be loaded.
_pending += 1

# Queue for reading.
_queue_read.put((collection, name, filepath, max_size, abort_signal))

# Start read threads if they're not running.
if not _thread_stop_signal:
_thread_stop_signal = Event()

for _ in range(max(min(cpu_count(), settings.MAX_THREADS), 1)):
thread = Thread(target=_read_thread, args=(_thread_stop_signal,))
thread.start()

# Register emplace timer if it's not running.
if not bpy.app.timers.is_registered(_emplace_timer):
bpy.app.timers.register(_emplace_timer, persistent=True)
Loading