Module: Fontist::Indexes::IndexMixin

Included in:
DefaultFamilyFontIndex, FilenameIndex, PreferredFamilyFontIndex
Defined in:
lib/fontist/indexes/index_mixin.rb

Overview

IndexMixin provides common functionality for font index classes.

Performance Optimization (Tech Debt)

This module uses a temporary Hash-based lookup cache during index building to avoid O(n²) performance when adding many entries. This is a workaround for Lutaml::Model::Collection’s Array-based storage.

The Problem

Lutaml::Model::Collection stores entries as an Array, which provides O(n) lookup when searching for existing keys. When building an index with thousands of entries, this creates O(n²) behavior:

  • 8867 font styles × average 2670 comparisons = ~23.6 million comparisons

  • Index building: ~26 seconds with Array lookup

The Workaround

During ‘build` and `build_with_formulas`, we maintain a temporary `@index_build_cache` Hash that provides O(1) lookups. After building, the cache is cleared.

  • Index building with Hash lookup: ~0.08 seconds

  • Speedup: 325× faster

The Proper Fix

This tech debt should be resolved by enhancing Lutaml::Model::Collection to support efficient key-based lookups. See the reproduction script at: ‘dev/lutaml_model_collection_lookup_benchmark.rb`

Related issue: github.com/lutaml/lutaml-model/issues/XXX

Defined Under Namespace

Modules: ClassMethods

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object



38
39
40
# File 'lib/fontist/indexes/index_mixin.rb', line 38

def self.included(base)
  base.extend(ClassMethods)
end

Instance Method Details

#add_formula(formula) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/fontist/indexes/index_mixin.rb', line 107

def add_formula(formula)
  # Accept FormulaV4, FormulaV5, or any object that responds to all_fonts
  unless formula.respond_to?(:all_fonts) && formula.respond_to?(:path)
    raise ArgumentError,
          "Expected formula-like object, got #{formula.class}"
  end

  formula.all_fonts.each do |font|
    font.styles.each do |style|
      add_index_formula(style, formula.path)
    end
  end

  entries
end

#add_index_formula(style, formula_path) ⇒ Object

Add a font style to the index with O(1) or O(n) lookup.

Uses ‘@index_build_cache` Hash for O(1) lookup during building, falling back to O(n) Array lookup for incremental updates.



132
133
134
135
136
137
138
139
# File 'lib/fontist/indexes/index_mixin.rb', line 132

def add_index_formula(style, formula_path)
  key = prepare_index_key(style)
  paths = prepare_formula_paths(formula_path)

  return if merge_existing_entry?(key, paths)

  create_and_add_entry(key, paths)
end

#buildObject

Build index by loading all formulas from disk. Uses Hash-based cache for O(1) lookups during building.



78
79
80
81
82
83
84
85
86
87
88
# File 'lib/fontist/indexes/index_mixin.rb', line 78

def build
  with_index_build_cache do
    Formula.all.each do |formula|
      add_formula(formula)
    end
  end

  to_file

  self
end

#build_with_formulas(formulas) ⇒ Object

Build index from pre-loaded formulas. Uses Hash-based cache for O(1) lookups during building.

This is the preferred method when formulas are already loaded, as it avoids re-loading from disk.



95
96
97
98
99
100
101
102
103
104
105
# File 'lib/fontist/indexes/index_mixin.rb', line 95

def build_with_formulas(formulas)
  with_index_build_cache do
    formulas.each do |formula|
      add_formula(formula)
    end
  end

  to_file

  self
end

#index_key_for_style(_style) ⇒ Object

Raises:

  • (NotImplementedError)


123
124
125
126
# File 'lib/fontist/indexes/index_mixin.rb', line 123

def index_key_for_style(_style)
  raise NotImplementedError,
        "index_key_for_style(style) must be implemented"
end

#load_formulas(key) ⇒ Object



141
142
143
# File 'lib/fontist/indexes/index_mixin.rb', line 141

def load_formulas(key)
  index_formulas(key).flat_map(&:to_full)
end

#load_index_formulas(key) ⇒ Object



145
146
147
# File 'lib/fontist/indexes/index_mixin.rb', line 145

def load_index_formulas(key)
  index_formulas(key)
end

#to_file(file_path = self.class.path) ⇒ Object



149
150
151
152
153
154
155
156
# File 'lib/fontist/indexes/index_mixin.rb', line 149

def to_file(file_path = self.class.path)
  # Use default path if file_path is nil
  file_path = self.class.path if file_path.nil?
  # puts "Writing index to #{file_path}"

  FileUtils.mkdir_p(File.dirname(file_path))
  File.write(file_path, to_yaml)
end