Skip to content

Commit

Permalink
made multithreading faster (#47)
Browse files Browse the repository at this point in the history
Co-authored-by: skwal <SkwalExe>
  • Loading branch information
SkwalExe authored Nov 24, 2023
1 parent f2d66c7 commit c20d910
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 48 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# 1.3.0 - Unreleased
# 1.3.0 - 2023-11-24

### Added
- Zoom out click mode

### Fixed
- Prevent clicks outside of the canvas to be registered as canvas clicks
- Weird zoom in behaviour

### Improved
- I made the rendering process a lot faster with a new algorithm, its my first time with multiprocessing

# 1.2.0 - 2023-11-24

Expand Down
2 changes: 1 addition & 1 deletion fractalistic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Terminal based fractal explorer, including Mandelbrot, Burning Ship, and Julia."""

__version__ = "1.2.0"
__version__ = "1.3.0"
88 changes: 48 additions & 40 deletions fractalistic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from textual.events import Click
from textual.color import Color
from textual import on
from textual import log
# ---------- Local imports
from . import fractals, colors, __version__
from .utils import (
Expand All @@ -16,20 +17,21 @@
from .settings import Settings, RenderSettings
from .command import Command, CommandIncrement, CommandIncrementArgParseResult
from .fractal_canv import FractalCanv
from .line_divergence_result import LineDivergenceResult
# ---------- Other imports
import os
from PIL import Image
from typing import Callable
import asyncio
from multiprocessing import Pool, Manager
from rich.rule import Rule
from time import monotonic, time
from time import monotonic, time, sleep
from gmpy2 import mpc, mpfr
from copy import deepcopy
import gmpy2
import toml
from math import ceil
import itertools
from typing import Generator
# --------------------


Expand Down Expand Up @@ -441,27 +443,33 @@ def action_screenshot_2(self, screenshot_size: Vec | None) -> None:
update_loading_bar=True,
threads=self.settings.screenshot_threads)

# If the screenshot was cancelled, None is returned
for line in result:
# None is returned if the screenshot was cancelled
if line is None:
break

for (x, divergence) in enumerate(line.values):
# Get a color from the result
if divergence == -1:
color = Color.parse("black")
else:
color = self.selected_color(divergence)

image.putpixel((x, line.y), color.rgb)

# If the screenshot wasn't cancelled, save the screenshot to a file,
# put a message in the log panel and wait one second to
# allow the user to see that the operation is finished successfully.
if not self.cancel_screenshot:
for (y, line) in enumerate(result):
for (x, divergence) in enumerate(line):
# Get a color from the result
if divergence == -1:
color = Color.parse("black")
else:
color = self.selected_color(divergence)

image.putpixel((x, y), color.rgb)

save_to = f"{self.selected_fractal.__name__}_screenshot_{int(time())}.png"
image.save(save_to)
self.call_after_refresh(
self.log_write,
f"Screenshot [{screenshot_width}x{screenshot_height}] saved to [on violet]{save_to}")

# Wait one second to allow the user to see that the operation is finished successfully
sleep(1)

self.progress_bar.add_class("hidden")
self.container.remove_class("hidden")

Expand Down Expand Up @@ -532,7 +540,7 @@ def get_divergence_matrix(
self, cell_size: mpc | None = None,
size: Vec | None = None,
threads: int | None = None,
update_loading_bar: bool = False) -> list[list[int]]:
update_loading_bar: bool = False) -> Generator[LineDivergenceResult, None, None]:

if threads is None:
threads = self.settings.threads
Expand All @@ -556,41 +564,40 @@ def get_divergence_matrix(
self.current_process_pool = Pool(processes=threads)

# Start the rendering processes
divergence_matrices_async = self.current_process_pool.starmap_async(
self.current_process_pool.starmap_async(
get_divergence_matrix,
[(chunk[0], chunk[1], render_settings, size, queue) for chunk in chunks],
chunksize=1)

rendered_lines = 0
while not divergence_matrices_async.ready():
finished_process_count = 0
while not finished_process_count == threads:

# Return None if the screenshot was cancelled, and terminate the processes
if self.cancel_screenshot:
self.current_process_pool.terminate()
return None

while not queue.empty() and update_loading_bar:
rendered_lines += 1
rendered_lines += 1

# A message is added to the queue everytime a line is rendered
queue.get()
# A message is added to the queue everytime a line is rendered
line = queue.get()

# Make the progress bar advance every 10 lines
if rendered_lines % 10 == 0:
self.progress_bar.advance()
# None is added to the queue when a process is finished
if line is None:
finished_process_count += 1
continue

# When the rendering is finished, get the result from each process
divergence_matrices = divergence_matrices_async.get()
yield line

# Flatten the array of each process result into one big array
result = list(itertools.chain.from_iterable(divergence_matrices))
# Make the progress bar advance every 10 lines
if rendered_lines % 10 == 0 and update_loading_bar:
self.progress_bar.advance()

# Close the pool of processes
self.current_process_pool.close()
self.current_process_pool = None

return result

def load_state(self, filename: str) -> None:
try:
with open(filename, "r") as f:
Expand Down Expand Up @@ -823,26 +830,24 @@ def update_canv_(self) -> None:
self.renders += 1
start = monotonic()

result = self.get_divergence_matrix()
# If result is none, it means ctrl+c was pressed during the rendering
# then the program is exiting and we don't care anymore about updating the canvas
if result is None:
return

# Used to get the average divergence of the current render
divergence_sum = 0
term_count = 0

with self.batch_update():
for (y, line) in enumerate(result):
for (x, divergence) in enumerate(line):
for line in self.get_divergence_matrix():
# If none is returned, most likely the program is exiting
if line is None:
return

for (x, divergence) in enumerate(line.values):

# If there is a marker and the current x and y corresponds the its position
# make the pixel red and go to the next pixel
if self.settings.marker_pos is not None \
and x == self.settings.marker_pos.x \
and y == self.settings.marker_pos.y:
self.canv.set_pixel(x, y, Color.parse("red"))
and line.y == self.settings.marker_pos.y:
self.canv.set_pixel(x, line.y, Color.parse("red"))
continue

if divergence != -1:
Expand All @@ -851,7 +856,7 @@ def update_canv_(self) -> None:

# Get a color from the result
color = Color.parse("black") if divergence == -1 else self.selected_color(divergence)
self.canv.set_pixel(x, y, color)
self.canv.set_pixel(x, line.y, color)

self.average_divergence = divergence_sum / term_count if term_count > 0 else 0
self.current_zoom_level = f"{4 / (self.render_settings.cell_size * self.settings.canv_size.x):.4e}"
Expand Down Expand Up @@ -931,6 +936,9 @@ def compose(self):
self.rich_log.border_title = "Logs Panel"

async def on_ready(self) -> None:
# Just so that flake8 doesn't complain about log() being unused
log("Hellooo <3")

self.set_command_list()

# Mount the log panel and the command input in the right container
Expand Down
7 changes: 7 additions & 0 deletions fractalistic/line_divergence_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class LineDivergenceResult():
y: int
values: list[int]

def __init__(self, y: int, values: list[int]):
self.y = y
self.values = values
13 changes: 7 additions & 6 deletions fractalistic/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .vec import Vec
from .settings import RenderSettings
from multiprocessing import Queue
from .line_divergence_result import LineDivergenceResult
from gmpy2 import mpc
import gmpy2

Expand All @@ -27,21 +28,21 @@ def pos_to_c(pos: Vec, cell_size, screen_pos_on_plane, screen_size) -> mpc:
return result


def set_precision(value):
def set_precision(value) -> None:
gmpy2.get_context().precision = value


def get_divergence_matrix(start: int, stop: int, render_settings: RenderSettings, size: Vec, queue: Queue):
def get_divergence_matrix(start: int, stop: int, render_settings: RenderSettings, size: Vec, queue: Queue) -> None:
set_precision(render_settings.wanted_numeric_precision)
lines_to_render = stop - start
result = [[0 for x in range(size.x)] for y in range(lines_to_render)]
pos_on_plane = render_settings.screen_pos_on_plane

for y in range(lines_to_render):
result = [0] * size.x
for x in range(size.x):
pos = Vec(x, y+start)
c_num = pos_to_c(pos, render_settings.cell_size, pos_on_plane, size)
result[y][x] = fractal_list[render_settings.fractal_index].get(c_num, render_settings)
queue.put(None)
result[x] = fractal_list[render_settings.fractal_index].get(c_num, render_settings)
queue.put(LineDivergenceResult(y+start, result))

return result
queue.put(None)

0 comments on commit c20d910

Please sign in to comment.