svg_conform 0.1.9 → 0.1.11
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/.gitignore +4 -0
- data/.rubocop_todo.yml +36 -17
- data/benchmark/performance_comparison.rb +120 -0
- data/docs/performance.adoc +239 -0
- data/lib/svg_conform/classification_cache.rb +36 -0
- data/lib/svg_conform/element_proxy.rb +12 -7
- data/lib/svg_conform/profile.rb +5 -0
- data/lib/svg_conform/profile_compiler.rb +101 -0
- data/lib/svg_conform/profiles.rb +2 -3
- data/lib/svg_conform/requirements/allowed_elements_requirement.rb +84 -55
- data/lib/svg_conform/requirements/base_requirement.rb +83 -0
- data/lib/svg_conform/requirements/id_reference_requirement.rb +29 -28
- data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +41 -21
- data/lib/svg_conform/requirements/no_external_css_requirement.rb +16 -9
- data/lib/svg_conform/sax_validation_handler.rb +75 -36
- data/lib/svg_conform/validation/tracker_factory.rb +55 -0
- data/lib/svg_conform/validation_context.rb +60 -24
- data/lib/svg_conform/validator.rb +38 -17
- data/lib/svg_conform/version.rb +1 -1
- data/spec/fixtures/svg_1_2_rfc/expected_errors/ietf_test_violations.yml +47 -0
- data/spec/fixtures/svg_1_2_rfc/inputs/ietf_test_violations.svg +28 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb +150 -0
- data/spec/svg_conform/validation_context_spec.rb +305 -0
- data/spec/svg_conform_spec.rb +156 -0
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9300ab0026e52b929e0707e14c19ce45394da831dfd5e44f4afad14e1a2237cd
|
|
4
|
+
data.tar.gz: b19b0d2dbce1397abb478105704c994a32f418972240c9c149d375d8be519e2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d413baac7ebb781837ff482103d751ce3222c170c613707b8411d5c6614dff71ac874802e0e5df14e276c0d56f537d32259787559359dfb954f6e3392088102d
|
|
7
|
+
data.tar.gz: ca1a62500f77534fca87d5b0e0136328ed81b9d9a7a81510e312a3760f31157e7a42af4934bacda3af2d6915b675bb9f4afdba2746332922cb23351bcc47fba2
|
data/.gitignore
CHANGED
data/.rubocop_todo.yml
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-01-
|
|
3
|
+
# on 2026-01-27 00:27:35 UTC using RuboCop version 1.82.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
|
8
8
|
|
|
9
|
+
# Offense count: 1
|
|
10
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
11
|
+
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
|
12
|
+
# SupportedStyles: with_first_argument, with_fixed_indentation
|
|
13
|
+
Layout/ArgumentAlignment:
|
|
14
|
+
Exclude:
|
|
15
|
+
- 'lib/svg_conform/requirements/invalid_id_references_requirement.rb'
|
|
16
|
+
|
|
9
17
|
# Offense count: 1
|
|
10
18
|
# This cop supports safe autocorrection (--autocorrect).
|
|
11
19
|
# Configuration parameters: EnforcedStyleAlignWith.
|
|
@@ -14,7 +22,17 @@ Layout/BlockAlignment:
|
|
|
14
22
|
Exclude:
|
|
15
23
|
- 'spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb'
|
|
16
24
|
|
|
17
|
-
# Offense count:
|
|
25
|
+
# Offense count: 1
|
|
26
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
27
|
+
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
|
|
28
|
+
# SupportedHashRocketStyles: key, separator, table
|
|
29
|
+
# SupportedColonStyles: key, separator, table
|
|
30
|
+
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
|
31
|
+
Layout/HashAlignment:
|
|
32
|
+
Exclude:
|
|
33
|
+
- 'spec/svg_conform/validation_context_spec.rb'
|
|
34
|
+
|
|
35
|
+
# Offense count: 662
|
|
18
36
|
# This cop supports safe autocorrection (--autocorrect).
|
|
19
37
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
20
38
|
# URISchemes: http, https
|
|
@@ -22,6 +40,14 @@ Layout/LineLength:
|
|
|
22
40
|
Enabled: false
|
|
23
41
|
|
|
24
42
|
# Offense count: 2
|
|
43
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
44
|
+
# Configuration parameters: AllowInHeredoc.
|
|
45
|
+
Layout/TrailingWhitespace:
|
|
46
|
+
Exclude:
|
|
47
|
+
- 'lib/svg_conform/requirements/invalid_id_references_requirement.rb'
|
|
48
|
+
- 'spec/svg_conform/validation_context_spec.rb'
|
|
49
|
+
|
|
50
|
+
# Offense count: 3
|
|
25
51
|
# Configuration parameters: AllowedMethods.
|
|
26
52
|
# AllowedMethods: enums
|
|
27
53
|
Lint/ConstantDefinitionInBlock:
|
|
@@ -70,7 +96,7 @@ Lint/UnreachableCode:
|
|
|
70
96
|
Exclude:
|
|
71
97
|
- 'lib/svg_conform/commands/check.rb'
|
|
72
98
|
|
|
73
|
-
# Offense count:
|
|
99
|
+
# Offense count: 146
|
|
74
100
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
75
101
|
Metrics/AbcSize:
|
|
76
102
|
Enabled: false
|
|
@@ -81,17 +107,17 @@ Metrics/AbcSize:
|
|
|
81
107
|
Metrics/BlockLength:
|
|
82
108
|
Max: 253
|
|
83
109
|
|
|
84
|
-
# Offense count:
|
|
110
|
+
# Offense count: 2
|
|
85
111
|
# Configuration parameters: CountBlocks, CountModifierForms.
|
|
86
112
|
Metrics/BlockNesting:
|
|
87
113
|
Max: 4
|
|
88
114
|
|
|
89
|
-
# Offense count:
|
|
115
|
+
# Offense count: 125
|
|
90
116
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
91
117
|
Metrics/CyclomaticComplexity:
|
|
92
118
|
Enabled: false
|
|
93
119
|
|
|
94
|
-
# Offense count:
|
|
120
|
+
# Offense count: 259
|
|
95
121
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
96
122
|
Metrics/MethodLength:
|
|
97
123
|
Max: 154
|
|
@@ -101,18 +127,11 @@ Metrics/MethodLength:
|
|
|
101
127
|
Metrics/ParameterLists:
|
|
102
128
|
Max: 9
|
|
103
129
|
|
|
104
|
-
# Offense count:
|
|
130
|
+
# Offense count: 99
|
|
105
131
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
106
132
|
Metrics/PerceivedComplexity:
|
|
107
133
|
Enabled: false
|
|
108
134
|
|
|
109
|
-
# Offense count: 4
|
|
110
|
-
# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns.
|
|
111
|
-
# SupportedStyles: snake_case, camelCase
|
|
112
|
-
Naming/VariableName:
|
|
113
|
-
Exclude:
|
|
114
|
-
- 'check_clippath_font.rb'
|
|
115
|
-
|
|
116
135
|
# Offense count: 2
|
|
117
136
|
# Configuration parameters: MinSize.
|
|
118
137
|
Performance/CollectionLiteralInLoop:
|
|
@@ -139,12 +158,12 @@ RSpec/DescribeClass:
|
|
|
139
158
|
- 'spec/svg_conform/references/integration_spec.rb'
|
|
140
159
|
- 'spec/svgcheck_compatibility_spec.rb'
|
|
141
160
|
|
|
142
|
-
# Offense count:
|
|
161
|
+
# Offense count: 157
|
|
143
162
|
# Configuration parameters: CountAsOne.
|
|
144
163
|
RSpec/ExampleLength:
|
|
145
164
|
Max: 53
|
|
146
165
|
|
|
147
|
-
# Offense count:
|
|
166
|
+
# Offense count: 3
|
|
148
167
|
RSpec/LeakyConstantDeclaration:
|
|
149
168
|
Exclude:
|
|
150
169
|
- 'spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb'
|
|
@@ -160,7 +179,7 @@ RSpec/MultipleDescribes:
|
|
|
160
179
|
Exclude:
|
|
161
180
|
- 'spec/svg_conform/batch_report_spec.rb'
|
|
162
181
|
|
|
163
|
-
# Offense count:
|
|
182
|
+
# Offense count: 126
|
|
164
183
|
RSpec/MultipleExpectations:
|
|
165
184
|
Max: 8
|
|
166
185
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Performance benchmark for svg_conform optimizations
|
|
5
|
+
# Measures validation speed for single and batch operations
|
|
6
|
+
|
|
7
|
+
require "bundler/setup"
|
|
8
|
+
require "benchmark"
|
|
9
|
+
require_relative "../lib/svg_conform"
|
|
10
|
+
|
|
11
|
+
# Helper method to create test SVG content
|
|
12
|
+
def create_test_svg(element_count: 10, attribute_count: 5)
|
|
13
|
+
elements = (1..element_count).map do |i|
|
|
14
|
+
attrs = (1..attribute_count).map { |j| "attr#{j}='value#{j}'" }.join(" ")
|
|
15
|
+
"<rect id='rect#{i}' x='#{i * 10}' y='#{i * 10}' width='10' height='10' #{attrs}/>"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
<<~SVG
|
|
19
|
+
<?xml version="1.0"?>
|
|
20
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
|
|
21
|
+
#{elements.join("\n ")}
|
|
22
|
+
</svg>
|
|
23
|
+
SVG
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
puts "=" * 80
|
|
27
|
+
puts "svg_conform Performance Benchmark"
|
|
28
|
+
puts "=" * 80
|
|
29
|
+
puts
|
|
30
|
+
|
|
31
|
+
# Test configurations
|
|
32
|
+
test_cases = [
|
|
33
|
+
{ name: "Small document", elements: 10, attributes: 3 },
|
|
34
|
+
{ name: "Medium document", elements: 50, attributes: 5 },
|
|
35
|
+
{ name: "Large document", elements: 100, attributes: 8 },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
validator = SvgConform::Validator.new
|
|
39
|
+
|
|
40
|
+
test_cases.each do |test_case|
|
|
41
|
+
puts "Test: #{test_case[:name]}"
|
|
42
|
+
puts " Elements: #{test_case[:elements]}, Attributes per element: #{test_case[:attributes]}"
|
|
43
|
+
puts
|
|
44
|
+
|
|
45
|
+
svg_content = create_test_svg(element_count: test_case[:elements],
|
|
46
|
+
attribute_count: test_case[:attributes])
|
|
47
|
+
|
|
48
|
+
# Warmup run
|
|
49
|
+
validator.validate(svg_content, profile: :svg_1_2_rfc)
|
|
50
|
+
|
|
51
|
+
# Benchmark single validation
|
|
52
|
+
single_time = Benchmark.measure do
|
|
53
|
+
100.times do
|
|
54
|
+
validator.validate(svg_content, profile: :svg_1_2_rfc)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts " Single validation (100 runs): #{(single_time.real * 1000).round(2)}ms"
|
|
59
|
+
puts " Average per validation: #{(single_time.real * 10).round(2)}ms"
|
|
60
|
+
|
|
61
|
+
# Benchmark batch validation
|
|
62
|
+
contents = Array.new(10) do
|
|
63
|
+
create_test_svg(element_count: test_case[:elements],
|
|
64
|
+
attribute_count: test_case[:attributes])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
batch_time = Benchmark.measure do
|
|
68
|
+
10.times do
|
|
69
|
+
contents.each do |content|
|
|
70
|
+
validator.validate(content, profile: :svg_1_2_rfc)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
puts " Batch validation (10 files × 10 runs): #{(batch_time.real * 1000).round(2)}ms"
|
|
76
|
+
puts " Average per file: #{(batch_time.real * 100).round(2)}ms"
|
|
77
|
+
puts
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
puts "=" * 80
|
|
81
|
+
puts "Memory Profiling"
|
|
82
|
+
puts "=" * 80
|
|
83
|
+
puts
|
|
84
|
+
|
|
85
|
+
if Object.const_defined?(:GC)
|
|
86
|
+
GC.start
|
|
87
|
+
before_mem = `ps -o rss= -p #{Process.pid}`.to_i
|
|
88
|
+
|
|
89
|
+
# Create and validate 100 documents
|
|
90
|
+
100.times do
|
|
91
|
+
svg = create_test_svg(element_count: 50, attribute_count: 5)
|
|
92
|
+
validator.validate(svg, profile: :svg_1_2_rfc)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
GC.start
|
|
96
|
+
after_mem = `ps -o rss= -p #{Process.pid}`.to_i
|
|
97
|
+
|
|
98
|
+
mem_increase = after_mem - before_mem
|
|
99
|
+
puts " RSS memory increase: #{mem_increase} KB"
|
|
100
|
+
puts " Per validation: #{(mem_increase / 100.0).round(2)} KB"
|
|
101
|
+
else
|
|
102
|
+
puts " Memory profiling not available on this platform"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
puts
|
|
106
|
+
puts "=" * 80
|
|
107
|
+
puts "Benchmark Complete"
|
|
108
|
+
puts "=" * 80
|
|
109
|
+
puts
|
|
110
|
+
puts "Optimizations tested:"
|
|
111
|
+
puts " ✓ Phase 1.1: ElementProxy#path_id memoization"
|
|
112
|
+
puts " ✓ Phase 1.2: Element configuration index"
|
|
113
|
+
puts " ✓ Phase 2.1: Requirement classification cache"
|
|
114
|
+
puts " ✓ Phase 2.2: Global properties constant"
|
|
115
|
+
puts " ✓ Phase 2.3: ElementProxy attributes cache"
|
|
116
|
+
puts " ✓ Phase 3.1: Configuration validation cache"
|
|
117
|
+
puts " ✓ Phase 3.3: ProfileCompiler class"
|
|
118
|
+
puts " ✓ Phase 4.1: TrackerFactory extraction"
|
|
119
|
+
puts " ✓ Phase 5.1: Batch validation optimization"
|
|
120
|
+
puts
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
= Performance Optimization Guide
|
|
2
|
+
|
|
3
|
+
This guide documents the performance optimization patterns used in svg_conform.
|
|
4
|
+
|
|
5
|
+
== Goals
|
|
6
|
+
|
|
7
|
+
The performance optimization strategy focuses on:
|
|
8
|
+
|
|
9
|
+
* Reducing validation time for repeated operations
|
|
10
|
+
* Minimizing memory allocations during SAX parsing
|
|
11
|
+
* Caching expensive operations without introducing complexity
|
|
12
|
+
* Maintaining clean separation of concerns
|
|
13
|
+
|
|
14
|
+
== Optimization Patterns
|
|
15
|
+
|
|
16
|
+
=== Memoization
|
|
17
|
+
|
|
18
|
+
Cache computed values to avoid repeated calculations.
|
|
19
|
+
|
|
20
|
+
*Example:* `ElementProxy#path_id` uses `@cached_path_id` to store the computed path:
|
|
21
|
+
|
|
22
|
+
[source,ruby]
|
|
23
|
+
----
|
|
24
|
+
def path_id
|
|
25
|
+
@cached_path_id ||= begin
|
|
26
|
+
parts = @path + ["#{@name}[#{@position]}"]
|
|
27
|
+
"/#{parts.join('/')}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
----
|
|
31
|
+
|
|
32
|
+
*When to use:* When a method is called multiple times with the same result and the computation is expensive.
|
|
33
|
+
|
|
34
|
+
*When to avoid:* When the result changes between calls, or the computation is trivial.
|
|
35
|
+
|
|
36
|
+
=== Class-Level Caching with Thread Safety
|
|
37
|
+
|
|
38
|
+
Use class-level caches with Mutex for thread-safe shared state.
|
|
39
|
+
|
|
40
|
+
*Example:* Requirement classification cache in `SaxValidationHandler`:
|
|
41
|
+
|
|
42
|
+
[source,ruby]
|
|
43
|
+
----
|
|
44
|
+
@classification_cache = {}
|
|
45
|
+
@classification_cache_mutex = Mutex.new
|
|
46
|
+
|
|
47
|
+
def classify_requirements_with_cache
|
|
48
|
+
profile_key = @profile.class.name
|
|
49
|
+
classified = self.class.classification_cache_mutex.synchronize do
|
|
50
|
+
self.class.classification_cache[profile_key]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ... compute and cache if not found
|
|
54
|
+
end
|
|
55
|
+
----
|
|
56
|
+
|
|
57
|
+
*When to use:* When the cached value is shared across all instances and doesn't change.
|
|
58
|
+
|
|
59
|
+
*When to avoid:* When the cached value is instance-specific or changes frequently.
|
|
60
|
+
|
|
61
|
+
=== Frozen Constants
|
|
62
|
+
|
|
63
|
+
Define immutable data as frozen constants to avoid reallocations.
|
|
64
|
+
|
|
65
|
+
*Example:* `GLOBAL_PROPERTIES` constant in `AllowedElementsRequirement`:
|
|
66
|
+
|
|
67
|
+
[source,ruby]
|
|
68
|
+
----
|
|
69
|
+
GLOBAL_PROPERTIES = %w[
|
|
70
|
+
about base baseprofile d break class content cx cy
|
|
71
|
+
# ... full list of properties
|
|
72
|
+
].freeze
|
|
73
|
+
----
|
|
74
|
+
|
|
75
|
+
*When to use:* For lookup tables, configuration data, and other immutable collections.
|
|
76
|
+
|
|
77
|
+
*When to avoid:* When the data needs to be modified at runtime.
|
|
78
|
+
|
|
79
|
+
=== Hash Indexes for O(1) Lookups
|
|
80
|
+
|
|
81
|
+
Pre-build hash indexes for fast lookups instead of linear array searches.
|
|
82
|
+
|
|
83
|
+
*Example:* Element configuration index in `AllowedElementsRequirement`:
|
|
84
|
+
|
|
85
|
+
[source,ruby]
|
|
86
|
+
----
|
|
87
|
+
def after_initialize
|
|
88
|
+
build_element_config_index if element_configs&.any?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_element_config_index
|
|
92
|
+
@element_config_index = {}
|
|
93
|
+
element_configs.each do |config|
|
|
94
|
+
@element_config_index[config.tag] = config
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Usage - O(1) instead of O(n)
|
|
99
|
+
element_config = @element_config_index&.dig(element_name)
|
|
100
|
+
----
|
|
101
|
+
|
|
102
|
+
*When to use:* When frequently looking up items by key in a collection.
|
|
103
|
+
|
|
104
|
+
*When to avoid:* When the collection is small or accessed sequentially.
|
|
105
|
+
|
|
106
|
+
=== Lazy Initialization with `||=`
|
|
107
|
+
|
|
108
|
+
Use the "or-equals" operator for lazy initialization.
|
|
109
|
+
|
|
110
|
+
*Example:* Cached attributes array in `ElementProxy`:
|
|
111
|
+
|
|
112
|
+
[source,ruby]
|
|
113
|
+
----
|
|
114
|
+
def attributes
|
|
115
|
+
@cached_attributes ||= @raw_attributes.map { |name, value| SaxAttribute.new(name, value) }
|
|
116
|
+
end
|
|
117
|
+
----
|
|
118
|
+
|
|
119
|
+
*When to use:* When a value is expensive to compute and may not be needed.
|
|
120
|
+
|
|
121
|
+
*When to avoid:* When `nil` or `false` are valid cached values (use explicit nil check instead).
|
|
122
|
+
|
|
123
|
+
=== Batch Processing Optimization
|
|
124
|
+
|
|
125
|
+
Amortize fixed costs across multiple operations.
|
|
126
|
+
|
|
127
|
+
*Example:* Batch file validation in `Validator`:
|
|
128
|
+
|
|
129
|
+
[source,ruby]
|
|
130
|
+
----
|
|
131
|
+
def validate_files(file_paths, profile: :svg_1_2_rfc, **options)
|
|
132
|
+
# Load profile once
|
|
133
|
+
profile_obj = resolve_profile(profile)
|
|
134
|
+
|
|
135
|
+
file_paths.each do |file_path|
|
|
136
|
+
validate_file_with_profile(file_path, profile_obj, **options)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
----
|
|
140
|
+
|
|
141
|
+
*When to use:* When processing multiple items with shared setup costs.
|
|
142
|
+
|
|
143
|
+
*When to avoid:* When processing single items or when items have different configurations.
|
|
144
|
+
|
|
145
|
+
=== Factory Pattern for Complex Initialization
|
|
146
|
+
|
|
147
|
+
Centralize object creation for cleaner initialization.
|
|
148
|
+
|
|
149
|
+
*Example:* `TrackerFactory` for creating validation trackers:
|
|
150
|
+
|
|
151
|
+
[source,ruby]
|
|
152
|
+
----
|
|
153
|
+
module Validation
|
|
154
|
+
module TrackerFactory
|
|
155
|
+
def self.create_all_trackers(document)
|
|
156
|
+
node_id_manager = create_node_id_manager(document)
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
error_tracker: create_error_tracker,
|
|
160
|
+
node_id_manager: node_id_manager,
|
|
161
|
+
# ... other trackers
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
----
|
|
167
|
+
|
|
168
|
+
*When to use:* When initialization involves multiple related objects or complex logic.
|
|
169
|
+
|
|
170
|
+
*When to avoid:* For simple object creation that doesn't benefit from centralization.
|
|
171
|
+
|
|
172
|
+
== Anti-Patterns to Avoid
|
|
173
|
+
|
|
174
|
+
=== Premature Optimization
|
|
175
|
+
|
|
176
|
+
Don't optimize without benchmarks. Measure first, then optimize.
|
|
177
|
+
|
|
178
|
+
=== Global Mutable State
|
|
179
|
+
|
|
180
|
+
Avoid mutable global state. Use class-level caches with thread-safe access.
|
|
181
|
+
|
|
182
|
+
=== Over-Abstraction
|
|
183
|
+
|
|
184
|
+
Don't create abstractions just for the sake of it. Each abstraction should pull its weight.
|
|
185
|
+
|
|
186
|
+
=== Optimizing Uncommon Paths
|
|
187
|
+
|
|
188
|
+
Focus on hot paths. Don't optimize code that rarely runs.
|
|
189
|
+
|
|
190
|
+
=== Sacrificing Readability
|
|
191
|
+
|
|
192
|
+
Performance gains should not come at the cost of code clarity.
|
|
193
|
+
|
|
194
|
+
== Performance Metrics
|
|
195
|
+
|
|
196
|
+
The `benchmark/performance_comparison.rb` script measures:
|
|
197
|
+
|
|
198
|
+
* Single validation speed (average time per document)
|
|
199
|
+
* Batch validation speed (average time per file)
|
|
200
|
+
* Memory usage per validation
|
|
201
|
+
|
|
202
|
+
Run benchmarks before and after optimizations to measure impact:
|
|
203
|
+
|
|
204
|
+
[source,shell]
|
|
205
|
+
----
|
|
206
|
+
ruby benchmark/performance_comparison.rb
|
|
207
|
+
----
|
|
208
|
+
|
|
209
|
+
== Architecture Considerations
|
|
210
|
+
|
|
211
|
+
=== SAX vs DOM Validation
|
|
212
|
+
|
|
213
|
+
svg_conform uses SAX validation by default for better performance:
|
|
214
|
+
|
|
215
|
+
* *SAX mode*: Constant memory, streaming parser, handles large files
|
|
216
|
+
* *DOM mode*: Loads full document, uses more memory, slower for large files
|
|
217
|
+
|
|
218
|
+
Always prefer SAX for validation. Only use DOM when applying remediations.
|
|
219
|
+
|
|
220
|
+
=== Caching Strategy
|
|
221
|
+
|
|
222
|
+
* *Instance-level cache*: Use for data specific to a validation run
|
|
223
|
+
* *Class-level cache*: Use for data shared across all instances
|
|
224
|
+
* *Thread safety*: Always use Mutex for class-level shared state
|
|
225
|
+
|
|
226
|
+
== Future Optimization Opportunities
|
|
227
|
+
|
|
228
|
+
Areas that may benefit from future optimization:
|
|
229
|
+
|
|
230
|
+
* Parallel batch validation for multi-core systems
|
|
231
|
+
* Compiled profiles with pre-computed lookup tables
|
|
232
|
+
* Streaming remediation to avoid full DOM load
|
|
233
|
+
* Attribute name interning for reduced memory
|
|
234
|
+
|
|
235
|
+
== Further Reading
|
|
236
|
+
|
|
237
|
+
* `PERFORMANCE_OPTIMIZATION_PLAN.md` - Detailed optimization plan
|
|
238
|
+
* `CLAUDE.md` - Architecture overview
|
|
239
|
+
* `benchmark/performance_comparison.rb` - Performance benchmarks
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SvgConform
|
|
4
|
+
# Cache for requirement classification results
|
|
5
|
+
# Instance-level (per SaxValidationHandler) to avoid shared mutable state
|
|
6
|
+
# No mutexes needed - each handler has its own cache
|
|
7
|
+
class ClassificationCache
|
|
8
|
+
def initialize
|
|
9
|
+
@cache = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Fetch from cache or compute the value
|
|
13
|
+
def fetch(key)
|
|
14
|
+
@cache[key] ||= yield
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Clear specific key or entire cache
|
|
18
|
+
def clear(key = nil)
|
|
19
|
+
if key
|
|
20
|
+
@cache.delete(key)
|
|
21
|
+
else
|
|
22
|
+
@cache.clear
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Return number of cached entries
|
|
27
|
+
def size
|
|
28
|
+
@cache.size
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if key exists in cache
|
|
32
|
+
def key?(key)
|
|
33
|
+
@cache.key?(key)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -32,15 +32,20 @@ module SvgConform
|
|
|
32
32
|
@child_counters = {} # Track child element positions
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# Build full path ID for this element
|
|
35
|
+
# Build full path ID for this element (memoized for performance)
|
|
36
36
|
def path_id
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
@path_id ||= begin
|
|
38
|
+
parts = @path + ["#{@name}[#{@position}]"]
|
|
39
|
+
"/#{parts.join('/')}"
|
|
40
|
+
end
|
|
39
41
|
end
|
|
40
42
|
|
|
41
|
-
# Return attributes as array of SaxAttribute objects (for
|
|
43
|
+
# Return attributes as array of SaxAttribute objects (memoized for performance)
|
|
44
|
+
# Cached to avoid repeated object allocations during SAX parsing
|
|
42
45
|
def attributes
|
|
43
|
-
@raw_attributes.map
|
|
46
|
+
@attributes ||= @raw_attributes.map do |name, value|
|
|
47
|
+
SaxAttribute.new(name, value)
|
|
48
|
+
end
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
# Check if this element has a specific attribute
|
|
@@ -75,8 +80,8 @@ module SvgConform
|
|
|
75
80
|
# Boolean check
|
|
76
81
|
has_attribute?(method.to_s.chomp("?"))
|
|
77
82
|
else
|
|
78
|
-
# Attribute access
|
|
79
|
-
@
|
|
83
|
+
# Attribute access - use raw_attributes hash, not cached array
|
|
84
|
+
@raw_attributes[method.to_s] || @raw_attributes[method.to_sym]
|
|
80
85
|
end
|
|
81
86
|
end
|
|
82
87
|
|
data/lib/svg_conform/profile.rb
CHANGED
|
@@ -6,7 +6,12 @@ require_relative "remediations"
|
|
|
6
6
|
|
|
7
7
|
module SvgConform
|
|
8
8
|
# Base class for SVG validation profiles using lutaml-model serialization
|
|
9
|
+
#
|
|
10
|
+
# Profile acts as a factory for requirements and remediations.
|
|
11
|
+
# Each validation creates fresh instances to avoid state pollution.
|
|
9
12
|
class Profile < Lutaml::Model::Serializable
|
|
13
|
+
PROFILES_DIR = File.expand_path("../../config/profiles", __dir__)
|
|
14
|
+
|
|
10
15
|
attribute :name, :string
|
|
11
16
|
attribute :description, :string
|
|
12
17
|
attribute :import, :string
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "profile"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
# Compiles and prepares SVG validation profiles
|
|
7
|
+
# Centralizes profile loading logic for better separation of concerns
|
|
8
|
+
class ProfileCompiler
|
|
9
|
+
@compile_cache = {}
|
|
10
|
+
@compile_mutex = Mutex.new
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
attr_reader :compile_cache, :compile_mutex
|
|
14
|
+
|
|
15
|
+
# Compile a profile by name
|
|
16
|
+
# Returns a cached profile if already compiled
|
|
17
|
+
def compile(profile_id)
|
|
18
|
+
profile_key = profile_id.to_s
|
|
19
|
+
|
|
20
|
+
# Check cache (thread-safe)
|
|
21
|
+
compile_mutex.synchronize do
|
|
22
|
+
return compile_cache[profile_key] if compile_cache[profile_key]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Load and compile profile
|
|
26
|
+
profile = load_profile(profile_key)
|
|
27
|
+
compiled_profile = prepare_profile(profile)
|
|
28
|
+
|
|
29
|
+
# Cache result (thread-safe)
|
|
30
|
+
compile_mutex.synchronize do
|
|
31
|
+
compile_cache[profile_key] = compiled_profile
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
compiled_profile
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Compile from YAML content
|
|
38
|
+
def compile_from_yaml(yaml_content)
|
|
39
|
+
profile = Profile.from_yaml(yaml_content)
|
|
40
|
+
prepare_profile(profile)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Compile from file path
|
|
44
|
+
def compile_from_file(file_path)
|
|
45
|
+
profile = Profile.load_from_file(file_path)
|
|
46
|
+
prepare_profile(profile)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Clear compilation cache
|
|
50
|
+
def clear_cache!
|
|
51
|
+
compile_mutex.synchronize do
|
|
52
|
+
compile_cache.clear
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Load profile from profiles directory
|
|
59
|
+
def load_profile(profile_name)
|
|
60
|
+
profile_file = File.join(Profile::PROFILES_DIR, "#{profile_name}.yml")
|
|
61
|
+
|
|
62
|
+
unless File.exist?(profile_file)
|
|
63
|
+
raise ProfileError,
|
|
64
|
+
"Profile not found: #{profile_name} (expected: #{profile_file})"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Profile.load_from_file(profile_file)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Prepare profile for use (validation and preprocessing)
|
|
71
|
+
def prepare_profile(profile)
|
|
72
|
+
# Validate profile has required fields
|
|
73
|
+
validate_profile(profile)
|
|
74
|
+
|
|
75
|
+
# Pre-compute requirement classifications for SAX validation
|
|
76
|
+
precompute_requirement_metadata(profile)
|
|
77
|
+
|
|
78
|
+
profile
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Validate profile structure
|
|
82
|
+
def validate_profile(profile)
|
|
83
|
+
if profile.name.nil? || profile.name.empty?
|
|
84
|
+
raise ProfileError,
|
|
85
|
+
"Profile must have a name"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Pre-compute metadata for faster validation
|
|
90
|
+
def precompute_requirement_metadata(profile)
|
|
91
|
+
# Trigger requirement classification to populate caches
|
|
92
|
+
profile.requirements.each do |req|
|
|
93
|
+
# Access classification method to populate SaxValidationHandler cache
|
|
94
|
+
if req.respond_to?(:needs_deferred_validation?)
|
|
95
|
+
req.needs_deferred_validation?
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|