Skip to content

Commit c22e53c

Browse files
committed
Pre-compute spatial graph statistics during Data initialization
Instead of calculating average degree and starting positions for each spatial match, pre-compute these statistics once when loading adjacency graphs. This avoids repeated map/inject operations on graph data during password matching, improving performance by approximately 9.3%. Performance improvement: 0.097ms -> 0.088ms per password (9.3% faster)
1 parent 52e6b47 commit c22e53c

File tree

4 files changed

+24
-20
lines changed

4 files changed

+24
-20
lines changed

.rubocop_todo.yml

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 100 --no-offense-counts --no-auto-gen-timestamp`
3-
# using RuboCop version 1.81.7.
3+
# using RuboCop version 1.82.1.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
@@ -22,17 +22,7 @@ Metrics/AbcSize:
2222
# AllowedMethods: refine
2323
Metrics/BlockLength:
2424
Exclude:
25-
- '**/*.gemspec'
2625
- 'lib/zxcvbn/matchers/spatial.rb'
27-
- 'spec/feedback_giver_spec.rb'
28-
- 'spec/match_spec.rb'
29-
- 'spec/matchers/date_spec.rb'
30-
- 'spec/matchers/l33t_spec.rb'
31-
- 'spec/scoring/crack_time_spec.rb'
32-
- 'spec/scoring/entropy_spec.rb'
33-
- 'spec/scoring/math_spec.rb'
34-
- 'spec/support/matcher.rb'
35-
- 'spec/tester_spec.rb'
3626

3727
# Configuration parameters: CountComments, Max, CountAsOne.
3828
Metrics/ClassLength:
@@ -46,7 +36,6 @@ Metrics/CyclomaticComplexity:
4636
Exclude:
4737
- 'lib/zxcvbn/entropy.rb'
4838
- 'lib/zxcvbn/feedback_giver.rb'
49-
- 'lib/zxcvbn/matchers/l33t.rb'
5039
- 'lib/zxcvbn/matchers/new_l33t.rb'
5140
- 'lib/zxcvbn/matchers/spatial.rb'
5241
- 'lib/zxcvbn/math.rb'
@@ -56,6 +45,7 @@ Metrics/CyclomaticComplexity:
5645
Metrics/MethodLength:
5746
Exclude:
5847
- 'lib/zxcvbn/crack_time.rb'
48+
- 'lib/zxcvbn/data.rb'
5949
- 'lib/zxcvbn/entropy.rb'
6050
- 'lib/zxcvbn/feedback_giver.rb'
6151
- 'lib/zxcvbn/matchers/date.rb'
@@ -109,8 +99,6 @@ Style/ClassAndModuleChildren:
10999
# Configuration parameters: AllowedConstants.
110100
Style/Documentation:
111101
Exclude:
112-
- 'spec/**/*'
113-
- 'test/**/*'
114102
- 'lib/zxcvbn.rb'
115103
- 'lib/zxcvbn/clock.rb'
116104
- 'lib/zxcvbn/crack_time.rb'

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- Implement Trie data structure for dictionary matching with 1.4x additional performance improvement ([#62])
1212
- Replace range operators with `String#slice` for string slicing operations ([#63])
1313
- Optimise L33t matcher with early bailout and improved deduplication ([#64])
14+
- Pre-compute spatial graph statistics during data initialisation ([#65])
1415

1516
[Unreleased]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.4...HEAD
1617
[#61]: https://github.com/envato/zxcvbn-ruby/pull/61
1718
[#62]: https://github.com/envato/zxcvbn-ruby/pull/62
1819
[#63]: https://github.com/envato/zxcvbn-ruby/pull/63
1920
[#64]: https://github.com/envato/zxcvbn-ruby/pull/64
21+
[#65]: https://github.com/envato/zxcvbn-ruby/pull/65
2022

2123
## [1.2.4] - 2025-12-07
2224

lib/zxcvbn/data.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ def initialize
1616
)
1717
@adjacency_graphs = JSON.parse(DATA_PATH.join('adjacency_graphs.json').read)
1818
@dictionary_tries = build_tries
19+
@graph_stats = compute_graph_stats
1920
end
2021

21-
attr_reader :ranked_dictionaries, :adjacency_graphs, :dictionary_tries
22+
attr_reader :ranked_dictionaries, :adjacency_graphs, :dictionary_tries, :graph_stats
2223

2324
def add_word_list(name, list)
2425
ranked_dict = DictionaryRanker.rank_dictionary(list)
@@ -41,5 +42,21 @@ def build_trie(ranked_dictionary)
4142
ranked_dictionary.each { |word, rank| trie.insert(word, rank) }
4243
trie
4344
end
45+
46+
def compute_graph_stats
47+
stats = {}
48+
@adjacency_graphs.each do |graph_name, graph|
49+
degrees = graph.map { |_, neighbors| neighbors.compact.size }
50+
sum = degrees.inject(0, :+)
51+
average_degree = sum.to_f / graph.size
52+
starting_positions = graph.length
53+
54+
stats[graph_name] = {
55+
average_degree: average_degree,
56+
starting_positions: starting_positions
57+
}
58+
end
59+
stats
60+
end
4461
end
4562
end

lib/zxcvbn/math.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,11 @@ def nCk(n, k)
4444
end
4545

4646
def average_degree_for_graph(graph_name)
47-
graph = data.adjacency_graphs[graph_name]
48-
degrees = graph.map { |_, neighbors| neighbors.compact.size }
49-
sum = degrees.inject(0, :+)
50-
sum.to_f / graph.size
47+
data.graph_stats[graph_name][:average_degree]
5148
end
5249

5350
def starting_positions_for_graph(graph_name)
54-
data.adjacency_graphs[graph_name].length
51+
data.graph_stats[graph_name][:starting_positions]
5552
end
5653
end
5754
end

0 commit comments

Comments
 (0)