fontist 3.0.21 → 3.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/fontist/formula.rb +4 -2
- data/lib/fontist/from_file_cache.rb +61 -35
- data/lib/fontist/system_index.rb +12 -127
- data/lib/fontist/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7679ab0078523e90b72a82f1d3f9e082554161503beb83d33b623f252710efa6
|
|
4
|
+
data.tar.gz: d56a2424a86f676c7905d039b92f95498fe3413c16a3bbfce014cf9c9daffd2d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 55250a04825526f62b2760d3e37124e5d1ce664b011f027d28d615f412abb3a75a647670ec62cf7fa466da3b620cab5fa854c21aae34a04fd2493da03ef9df62
|
|
7
|
+
data.tar.gz: 8cc7ad8c6a085ed5d9c0d2adf3deacd15f58147f5034aa25ca833e6ce89f971bf78436165ea54b380e7774c8b19c296b092a6d618932fc4cb877b2e863d9792e
|
data/lib/fontist/formula.rb
CHANGED
|
@@ -105,8 +105,10 @@ module Fontist
|
|
|
105
105
|
|
|
106
106
|
# Parsing thousands of formula files dominates index rebuilds;
|
|
107
107
|
# worker threads overlap the libyaml C parse, which releases
|
|
108
|
-
# the GVL
|
|
109
|
-
|
|
108
|
+
# the GVL. The pool is capped like every other fontist pool, so
|
|
109
|
+
# worker counts never scale with host core counts
|
|
110
|
+
workers = [Parallel.processor_count, 8].min
|
|
111
|
+
formulas = Parallel.map(paths, in_threads: workers) do |path|
|
|
110
112
|
Formula.from_file(path)
|
|
111
113
|
end.compact
|
|
112
114
|
|
|
@@ -1,52 +1,78 @@
|
|
|
1
1
|
module Fontist
|
|
2
2
|
# Thread-safe, mtime-validated memoization for parsed YAML artifacts
|
|
3
|
-
# (formula index, formula files), which cost hundreds of milliseconds
|
|
4
|
-
# parse and are re-read on every font lookup.
|
|
3
|
+
# (formula index, formula files), which cost hundreds of milliseconds
|
|
4
|
+
# to parse and are re-read on every font lookup.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Retention is bounded: fontist runs embedded in long-lived host
|
|
7
|
+
# processes (metanorma compiles) under memory caps, so the cache is
|
|
8
|
+
# an LRU holding at most max_entries parsed documents instead of
|
|
9
|
+
# every file the process has ever touched (~350MB for a full
|
|
10
|
+
# formulas corpus). Returned instances are shared between callers
|
|
11
|
+
# and must be treated as read-only. Entries re-resolve automatically
|
|
12
|
+
# when the file's mtime changes (e.g. after `fontist update`
|
|
13
|
+
# rewrites the formulas repo).
|
|
9
14
|
module FromFileCache
|
|
10
|
-
|
|
11
|
-
mtime = File.mtime(path)
|
|
15
|
+
DEFAULT_MAX_ENTRIES = 64
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
class << self
|
|
18
|
+
attr_writer :max_entries
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
value = yield
|
|
20
|
+
def max_entries
|
|
21
|
+
@max_entries ||= DEFAULT_MAX_ENTRIES
|
|
22
|
+
end
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
def memoize(path)
|
|
25
|
+
mtime = File.mtime(path)
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
hit = mutex.synchronize { lookup(path, mtime) }
|
|
28
|
+
return hit if hit
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
# Parse without holding the lock so parallel readers (e.g.
|
|
31
|
+
# Formula.all over worker threads) overlap; concurrent misses on
|
|
32
|
+
# the same path may duplicate work but write identical values
|
|
33
|
+
value = yield
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
hit = mutex.synchronize { cache[path] }
|
|
32
|
-
hit&.fetch(:mtime, nil) == mtime ? hit[:value] : nil
|
|
33
|
-
end
|
|
35
|
+
store(path, mtime, value)
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
mutex.synchronize do
|
|
37
|
-
existing = cache[path]
|
|
38
|
-
if existing.nil? || existing[:mtime] != mtime
|
|
39
|
-
cache[path] = { mtime: mtime, value: value }
|
|
40
|
-
end
|
|
37
|
+
value
|
|
41
38
|
end
|
|
42
|
-
end
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
def clear
|
|
41
|
+
mutex.synchronize { cache.clear }
|
|
42
|
+
end
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
attr_reader :mutex
|
|
47
|
+
|
|
48
|
+
def mutex
|
|
49
|
+
@mutex ||= Mutex.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def cache
|
|
53
|
+
@cache ||= {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def lookup(path, mtime)
|
|
57
|
+
entry = cache[path]
|
|
58
|
+
return nil unless entry&.fetch(:mtime, nil) == mtime
|
|
59
|
+
|
|
60
|
+
cache.delete(path)
|
|
61
|
+
cache[path] = entry
|
|
62
|
+
|
|
63
|
+
entry[:value]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def store(path, mtime, value)
|
|
67
|
+
mutex.synchronize do
|
|
68
|
+
existing = cache[path]
|
|
69
|
+
return if existing&.fetch(:mtime, nil) == mtime
|
|
70
|
+
|
|
71
|
+
cache.delete(path)
|
|
72
|
+
cache.delete(cache.each_key.first) if cache.size >= max_entries
|
|
73
|
+
cache[path] = { mtime: mtime, value: value }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
50
76
|
end
|
|
51
77
|
end
|
|
52
78
|
end
|
data/lib/fontist/system_index.rb
CHANGED
|
@@ -100,39 +100,6 @@ module Fontist
|
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
# Thread-safe progress display for parallel index scans
|
|
104
|
-
class ScanProgress
|
|
105
|
-
SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].freeze
|
|
106
|
-
|
|
107
|
-
def initialize(total, verbose:, &on_step)
|
|
108
|
-
@total = total
|
|
109
|
-
@verbose = verbose
|
|
110
|
-
@on_step = on_step
|
|
111
|
-
@mutex = Mutex.new
|
|
112
|
-
@processed = 0
|
|
113
|
-
# Show minimal progress for large collections even in non-verbose mode
|
|
114
|
-
@minimal = !verbose && total > 100
|
|
115
|
-
@interval = total > 1000 ? 100 : 50
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def step(path)
|
|
119
|
-
@mutex.synchronize do
|
|
120
|
-
@processed += 1
|
|
121
|
-
if @verbose
|
|
122
|
-
@on_step&.call(@processed, path)
|
|
123
|
-
elsif @minimal && report?
|
|
124
|
-
Fontist.ui.say("Scanning fonts: #{@processed}/#{@total}...")
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
private
|
|
130
|
-
|
|
131
|
-
def report?
|
|
132
|
-
(@processed % @interval).zero? || @processed == @total
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
103
|
# {:path=>"/Library/Fonts/Arial Unicode.ttf",
|
|
137
104
|
# :full_name=>"Arial Unicode MS",
|
|
138
105
|
# :family_name=>"Arial Unicode MS",
|
|
@@ -514,7 +481,7 @@ module Fontist
|
|
|
514
481
|
to_file(@path)
|
|
515
482
|
end
|
|
516
483
|
|
|
517
|
-
def detect_paths(paths, verbose: false, stats: nil
|
|
484
|
+
def detect_paths(paths, verbose: false, stats: nil)
|
|
518
485
|
existing_fonts_by_path = fonts&.group_by(&:path) || {}
|
|
519
486
|
|
|
520
487
|
# Initialize stats if not provided
|
|
@@ -523,100 +490,18 @@ module Fontist
|
|
|
523
490
|
|
|
524
491
|
sorted_paths = paths.sort.uniq
|
|
525
492
|
|
|
526
|
-
#
|
|
527
|
-
#
|
|
528
|
-
#
|
|
529
|
-
#
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
#
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
493
|
+
# The scan runs sequentially. Fontisan parsing is GVL-bound Ruby,
|
|
494
|
+
# so measured on 600 cache-miss fonts a single thread (2.3s) is
|
|
495
|
+
# faster than eight threads (3.3s), and fontisan memoizes parses
|
|
496
|
+
# per process, which bounds rescan costs anyway. Concurrency here
|
|
497
|
+
# previously meant forks, whose inherited heaps transiently
|
|
498
|
+
# multiplied host RSS up to 9x inside memory-capped metanorma
|
|
499
|
+
# compiles (fontist#510), and Marshal crashes on cached entries
|
|
500
|
+
# (fontist#508); Windows additionally suffered fontisan tempfile
|
|
501
|
+
# races under concurrent scans. Sequential serves every case
|
|
502
|
+
# Sequential serves every case
|
|
503
|
+
process_paths_sequential(sorted_paths, existing_fonts_by_path,
|
|
537
504
|
verbose: verbose, stats: stats)
|
|
538
|
-
else
|
|
539
|
-
process_paths_sequential(sorted_paths, existing_fonts_by_path,
|
|
540
|
-
verbose: verbose, stats: stats)
|
|
541
|
-
end
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
def process_paths_parallel(sorted_paths, existing_fonts_by_path, verbose:,
|
|
545
|
-
stats:)
|
|
546
|
-
require "parallel"
|
|
547
|
-
|
|
548
|
-
# Auto-detect cores, cap at 8 for optimal I/O performance
|
|
549
|
-
num_cores = [Parallel.processor_count, 8].min
|
|
550
|
-
|
|
551
|
-
if verbose
|
|
552
|
-
puts Paint["Using parallel processing with #{num_cores} cores",
|
|
553
|
-
:cyan]
|
|
554
|
-
|
|
555
|
-
# Verbose runs keep threads so live progress and per-font stats
|
|
556
|
-
# stay visible; fontisan parsing is GVL-bound, so this mode is
|
|
557
|
-
# slow, but it is the human-attended path
|
|
558
|
-
process_paths_in_threads(sorted_paths, existing_fonts_by_path,
|
|
559
|
-
verbose: verbose, stats: stats,
|
|
560
|
-
num_workers: num_cores)
|
|
561
|
-
else
|
|
562
|
-
# Threads, not forked processes: a silent scan runs from inside
|
|
563
|
-
# long-lived host processes (e.g. metanorma, whose heap reaches
|
|
564
|
-
# hundreds of MB by the time fonts are looked up). Each fork
|
|
565
|
-
# inherits that heap, and its RSS is charged to the process group
|
|
566
|
-
# while it lives, so an 8-process scan transiently multiplies the
|
|
567
|
-
# host's footprint by up to 9x — measured 3.6GB against a 0.9GB
|
|
568
|
-
# parent during a metanorma compile (OOMs under cgroup caps).
|
|
569
|
-
# The ~1s-per-600-fonts scan penalty is bounded and paid only on
|
|
570
|
-
# cache-miss rebuilds. split_paths_by_reuse keeps cached entries
|
|
571
|
-
# in the parent: see its comment for the marshal constraint.
|
|
572
|
-
to_scan, cached_results = split_paths_by_reuse(
|
|
573
|
-
sorted_paths,
|
|
574
|
-
existing_fonts_by_path)
|
|
575
|
-
|
|
576
|
-
results = Parallel.map(to_scan, in_threads: num_cores) do |path|
|
|
577
|
-
detect_fonts(path, stats: nil)
|
|
578
|
-
end.flatten.compact
|
|
579
|
-
|
|
580
|
-
stats&.total_fonts = cached_results.size + results.size
|
|
581
|
-
cached_results + results
|
|
582
|
-
end
|
|
583
|
-
end
|
|
584
|
-
|
|
585
|
-
# The stat-only reuse check runs in the parent: cached entries can
|
|
586
|
-
# carry backrefs to their SystemIndexFontCollection (whose
|
|
587
|
-
# paths_loader is a Proc), and worker results are Marshal.dump'ed
|
|
588
|
-
# back across the fork boundary — crossing it with cached
|
|
589
|
-
# collections raises "no _dump_data is defined for class Proc".
|
|
590
|
-
# Workers therefore receive only to-scan paths, and the block
|
|
591
|
-
# handed to Parallel closes over nothing but its argument.
|
|
592
|
-
def split_paths_by_reuse(sorted_paths, existing_fonts_by_path)
|
|
593
|
-
unchanged, to_scan = sorted_paths.partition do |path|
|
|
594
|
-
cached = existing_fonts_by_path[path]
|
|
595
|
-
cached&.any? && file_unchanged?(path, cached.first)
|
|
596
|
-
end
|
|
597
|
-
cached_results = unchanged.flat_map do |path|
|
|
598
|
-
existing_fonts_by_path[path].to_a
|
|
599
|
-
end
|
|
600
|
-
|
|
601
|
-
[to_scan, cached_results]
|
|
602
|
-
end
|
|
603
|
-
|
|
604
|
-
def process_paths_in_threads(sorted_paths, existing_fonts_by_path,
|
|
605
|
-
verbose:, stats:, num_workers:)
|
|
606
|
-
progress = ScanProgress.new(sorted_paths.size, verbose: verbose) do |count, path|
|
|
607
|
-
display_progress(count, sorted_paths.size, path,
|
|
608
|
-
ScanProgress::SPINNER_CHARS)
|
|
609
|
-
end
|
|
610
|
-
|
|
611
|
-
results = Parallel.map(sorted_paths, in_threads: num_workers) do |path|
|
|
612
|
-
result = detect_font_with_cache(path, existing_fonts_by_path[path],
|
|
613
|
-
stats: stats)
|
|
614
|
-
progress.step(path)
|
|
615
|
-
result
|
|
616
|
-
end.flatten.compact
|
|
617
|
-
|
|
618
|
-
clear_progress_line if verbose
|
|
619
|
-
results
|
|
620
505
|
end
|
|
621
506
|
|
|
622
507
|
def process_paths_sequential(sorted_paths, existing_fonts_by_path,
|
data/lib/fontist/version.rb
CHANGED