108 lines
5.5 KiB
Python
108 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import concurrent.futures
|
|
import json
|
|
import multiprocessing
|
|
import pathlib
|
|
import time
|
|
|
|
import generator.engraving.cache
|
|
import generator.engraving.fonts
|
|
import generator.engraving.types
|
|
import generator.engraving.validation
|
|
import generator.engraving.worker
|
|
import generator.validation
|
|
|
|
def engrave_tree(root: pathlib.Path, fonts: pathlib.Path, *, cache_dir: pathlib.Path | None = None, jobs: int = 1, progress: bool = False) -> set[str]:
|
|
if type(jobs) is not int or not 1 <= jobs <= 16:
|
|
raise ValueError('engraving jobs must be between 1 and 16')
|
|
if cache_dir and (cache_dir.resolve().is_relative_to(root.resolve()) or root.resolve().is_relative_to(cache_dir.resolve())):
|
|
raise ValueError('engraving cache and generated assets must not overlap')
|
|
started = time.monotonic()
|
|
paths = sorted(root.rglob('*.json'))
|
|
validated_pages = set()
|
|
if not paths:
|
|
return validated_pages
|
|
family, style = generator.engraving.fonts.Fonts(fonts), generator.engraving.types.Style()
|
|
fingerprint = family.fingerprint(style)
|
|
renderer = generator.engraving.cache.renderer_fingerprint(family, style)
|
|
if cache_dir:
|
|
cache_dir = generator.engraving.cache.renderer_namespace(cache_dir, renderer)
|
|
if progress:
|
|
state = 'warm namespace' if cache_dir.is_dir() else 'cold namespace'
|
|
print(f'Engraving cache enabled: {cache_dir} ({state}; renderer {renderer})', flush=True)
|
|
elif progress:
|
|
print('Engraving cache disabled', flush=True)
|
|
totals = generator.engraving.worker.GroupResult()
|
|
missing, rejected, completed = 0, 0, 0
|
|
groups = {}
|
|
for path in paths:
|
|
try:
|
|
document = json.loads(path.read_text(encoding='utf-8'))
|
|
generator.validation.validate_document(document)
|
|
score_key = generator.engraving.cache.score_fingerprint(document, renderer)
|
|
destination = path.with_suffix('.pages')
|
|
generator.engraving.worker.clear_pages(destination)
|
|
entry = cache_dir / 'fullscore' / score_key if cache_dir else None
|
|
present = entry is not None and (entry.exists() or entry.is_symlink())
|
|
pages = generator.engraving.cache.restore_pages(entry, destination, validated_pages) if entry else None
|
|
if pages is not None:
|
|
try:
|
|
generator.engraving.worker.validate_cached_views(document, pages)
|
|
except (ValueError, TypeError, KeyError):
|
|
pages = None
|
|
if pages is None:
|
|
rejected += int(present)
|
|
missing += int(entry is not None and not present)
|
|
notation = generator.engraving.cache.notation_fingerprint(document, renderer)
|
|
groups.setdefault(notation, []).append((path, score_key))
|
|
else:
|
|
generator.engraving.worker.write_bundle(path, document, pages, fingerprint)
|
|
totals.fullscore += 1
|
|
completed += 1
|
|
except ValueError as error:
|
|
raise generator.engraving.types.EngravingError(f'{path.relative_to(root)}: {error}') from error
|
|
prepare_time = time.monotonic() - started
|
|
if progress:
|
|
print(f'Engraving grouped: {len(paths)} scores, {len(groups)} notation bodies, {totals.fullscore} fullscore hits', flush=True)
|
|
validation_time = 0.0
|
|
|
|
def finish(result):
|
|
nonlocal completed, validation_time
|
|
started = time.monotonic()
|
|
for path, score_key, pages in result.scores:
|
|
destination = path.with_suffix('.pages')
|
|
for page in pages:
|
|
generator.engraving.validation.check_page(destination / page['path'], validated_pages, expected_sha256=page['sha256'])
|
|
validation_time += time.monotonic() - started
|
|
if cache_dir:
|
|
started = time.monotonic()
|
|
stored = set()
|
|
for path, score_key, pages in result.scores:
|
|
if score_key not in stored:
|
|
generator.engraving.cache.store_pages(cache_dir / 'fullscore' / score_key, path.with_suffix('.pages'), pages)
|
|
stored.add(score_key)
|
|
result.timings['cache'] += time.monotonic() - started
|
|
for field in ('fullscore', 'bodyhits', 'bodies', 'renderedpages', 'rejected'):
|
|
setattr(totals, field, getattr(totals, field) + getattr(result, field))
|
|
for phase, elapsed in result.timings.items():
|
|
totals.timings[phase] += elapsed
|
|
completed += len(result.scores)
|
|
if progress:
|
|
print(f'Engraving: {completed}/{len(paths)} scores, fullscore {totals.fullscore}, bodyhits {totals.bodyhits}, bodies {totals.bodies}, renderedpages {totals.renderedpages}', flush=True)
|
|
|
|
jobs = min(jobs, len(groups))
|
|
if jobs <= 1:
|
|
for notation, group in groups.items():
|
|
finish(generator.engraving.worker.engrave_group(notation, group, root, fonts, cache_dir, fingerprint, progress))
|
|
else:
|
|
with concurrent.futures.ProcessPoolExecutor(max_workers=jobs, mp_context=multiprocessing.get_context('spawn')) as pool:
|
|
futures = [pool.submit(generator.engraving.worker.engrave_group, notation, group, root, fonts, cache_dir, fingerprint, progress) for notation, group in groups.items()]
|
|
for future in concurrent.futures.as_completed(futures):
|
|
finish(future.result())
|
|
if progress:
|
|
print(f'Engraving complete: {len(paths)} scores, {totals.fullscore} reused, {missing} missing, {rejected} rejected; fullscore {totals.fullscore}, bodyhits {totals.bodyhits}, bodies {totals.bodies}, renderedpages {totals.renderedpages}, rejected bodies {totals.rejected}', flush=True)
|
|
phases = {'prepare': prepare_time, **totals.timings, 'validation': validation_time}
|
|
print('Engraving timings: ' + ', '.join(f'{phase} {elapsed:.3f}s' for phase, elapsed in phases.items()) + f'; wall {time.monotonic() - started:.3f}s (worker phases summed)', flush=True)
|
|
return validated_pages
|