Skip to content

Commit a4de134

Browse files
committed
perf: various regex caching optimizations
1 parent d920e65 commit a4de134

File tree

10 files changed

+72
-47
lines changed

10 files changed

+72
-47
lines changed

lib/device_detector.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,16 @@ class DeviceDetector
3434
MAJOR_VERSION_4 = Gem::Version.new('4.0')
3535
MAJOR_VERSION_8 = Gem::Version.new('8.0')
3636

37+
REGEX_CACHE = ::DeviceDetector::MemoryCache.new({})
38+
private_constant :REGEX_CACHE
39+
3740
attr_reader :client_hint, :user_agent
3841

3942
def initialize(user_agent = nil, headers = nil)
4043
@parsers = {}
4144

4245
@vendor_fragment_parser = DeviceDetector::Parser::VendorFragment.new
46+
@operating_system_parser = DeviceDetector::Parser::OperatingSystem.new
4347

4448
add_parser(Parser::Client::FeedReader.new)
4549
add_parser(Parser::Client::MobileApp.new)
@@ -59,7 +63,7 @@ def initialize(user_agent = nil, headers = nil)
5963

6064
add_parser(Parser::Bot.new)
6165

62-
use(user_agent, headers)
66+
use(user_agent, headers) if user_agent || headers
6367
end
6468

6569
def name
@@ -175,7 +179,7 @@ def parse_bot
175179
end
176180

177181
def parse_os
178-
parser = Parser::OperatingSystem.new
182+
parser = @operating_system_parser
179183
parser.use(@user_agent, @client_hints)
180184

181185
@os = parser.parse
@@ -340,8 +344,11 @@ def add_parser(parser)
340344
end
341345

342346
def match_user_agent(regex)
343-
src = regex.gsub('/', '\/')
344-
regexp = Regexp.new("(?:^|[^A-Z_-])(?:#{src})", Regexp::IGNORECASE)
347+
regexp = REGEX_CACHE.get_or_set(regex) do
348+
src = regex.gsub('/', '\/')
349+
Regexp.new("(?:^|[^A-Z_-])(?:#{src})", Regexp::IGNORECASE)
350+
end
351+
345352
match = @user_agent.match(regexp)
346353
return unless match
347354

lib/device_detector/memory_cache.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
class DeviceDetector
44
class MemoryCache
5-
DEFAULT_MAX_KEYS = 5000
5+
DEFAULT_MAX_KEYS = 15_000
66
STORES_NIL_VALUE = :__is_nil__
77

88
attr_reader :data, :max_keys, :lock

lib/device_detector/parser/abstract_parser.rb

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ def self.client_hint_mapping
1313

1414
USER_AGENT_CLIENT_HINTS_FRAGMENT_REGEX = %r{Android (?:10[.\d]*; K(?: Build/|[;)])|1[1-5]\)) AppleWebKit}i
1515

16+
DESKTOP_FRAGMENT_REGEX = /(?:Windows (?:NT|IoT)|X11; Linux x86_64)/i
17+
DESKTOP_FRAGMENT_NON_DESKTOP_REGEX = %r{CE-HTML| Mozilla/|Andr[o0]id|Tablet|Mobile|iPhone|Windows Phone|ricoh|OculusBrowser|PicoBrowser|Lenovo|compatible; MSIE|Trident/|Tesla/|XBOX|FBMD/|ARM; ?([^)]+)}i
18+
1619
def use(uas, hints)
1720
@user_agent = uas
1821
@client_hints = hints
@@ -96,15 +99,8 @@ def user_agent_client_hints_fragment?
9699
end
97100

98101
def desktop_fragment?
99-
regex =
100-
[
101-
'CE-HTML',
102-
' Mozilla/|Andr[o0]id|Tablet|Mobile|iPhone|Windows Phone|ricoh|OculusBrowser',
103-
'PicoBrowser|Lenovo|compatible; MSIE|Trident/|Tesla/|XBOX|FBMD/|ARM; ?([^)]+)'
104-
].join('|')
105-
106-
match_user_agent('(?:Windows (?:NT|IoT)|X11; Linux x86_64)') &&
107-
!match_user_agent(regex)
102+
match_user_agent_r(DESKTOP_FRAGMENT_REGEX) &&
103+
!match_user_agent_r(DESKTOP_FRAGMENT_NON_DESKTOP_REGEX)
108104
end
109105

110106
def fixture_file
@@ -137,21 +133,24 @@ def load_regexes
137133
end
138134

139135
def pre_match_overall?
140-
overall_regex = REGEX_CACHE.get_or_set("overall-#{fixture_file}") do
141-
regex_list = load_regexes
142-
regex_list = regex_list.values if regex_list.is_a?(Hash)
143-
144-
full_regex = regex_list.reduce('') do |res, regex|
145-
if regex
146-
"#{res}|#{regex['regex']}"
147-
else
148-
res
149-
end
150-
end.delete_prefix('|')
136+
regex_from_user_agent_cache('overall') do
137+
overall_regex = REGEX_CACHE.get_or_set("overall-#{fixture_file}") do
138+
regex_list = load_regexes
139+
regex_list = regex_list.values if regex_list.is_a?(Hash)
140+
141+
full_regex = regex_list.reduce('') do |res, regex|
142+
if regex
143+
"#{res}|#{regex['regex']}"
144+
else
145+
res
146+
end
147+
end.delete_prefix('|')
148+
149+
build_regex_for_ua(full_regex)
150+
end
151151

152-
build_regex_for_ua(full_regex)
152+
match_user_agent_r(overall_regex)
153153
end
154-
match_user_agent_r(overall_regex)
155154
end
156155

157156
def match_user_agent(regex)

lib/device_detector/parser/bot.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ def parser_type
88
end
99

1010
def parse
11-
return nil unless pre_match_overall?
12-
1311
regex_from_user_agent_cache do
14-
regexes.detect do |regex|
12+
pre_match_overall? && regexes.detect do |regex|
1513
match_user_agent_r(regex[:regex])
1614
end
1715
end

lib/device_detector/parser/client/abstract_client_parser.rb

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,22 @@ def parser_type
1414
end
1515

1616
def parse
17-
return unless pre_match_overall?
17+
regex_from_user_agent_cache do
18+
next nil unless pre_match_overall?
1819

19-
regex, matches = regex_from_user_agent_cache do
20-
regexes.detect do |regex|
20+
regex, matches = regexes.detect do |regex|
2121
match = match_user_agent_r(regex[:regex])
2222
match ? break [regex, match] : nil
2323
end
24-
end
2524

26-
return nil unless regex
25+
next nil unless regex
2726

28-
{
29-
type: parser_name,
30-
name: build_by_match(regex[:name], matches),
31-
version: build_version(regex[:version].to_s, matches)
32-
}
27+
{
28+
type: parser_name,
29+
name: build_by_match(regex[:name], matches),
30+
version: build_version(regex[:version].to_s, matches)
31+
}
32+
end
3333
end
3434
end
3535
end

lib/device_detector/parser/client/browser.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ class DeviceDetector
66
module Parser
77
module Client
88
class Browser < AbstractClientParser
9-
def use(uas, hints)
9+
def initialize
1010
super
1111
@browser_hints = DeviceDetector::Parser::Client::Hint::BrowserHints.new
12+
end
13+
14+
def use(uas, hints)
15+
super
1216
@browser_hints.use(uas, hints)
1317
end
1418

lib/device_detector/parser/client/mobile_app.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ class DeviceDetector
66
module Parser
77
module Client
88
class MobileApp < AbstractClientParser
9-
def use(uas, hints)
9+
def initialize
1010
super
1111
@app_hints = DeviceDetector::Parser::Client::Hint::AppHints.new
12+
end
13+
14+
def use(uas, hints)
15+
super
1216
@app_hints.use(uas, hints)
1317
end
1418

lib/device_detector/parser/device/hbb_tv.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ class DeviceDetector
44
module Parser
55
module Device
66
class HbbTv < AbstractDeviceParser
7+
def initialize
8+
super
9+
@hbb_regex = build_regex_for_ua('(?:HbbTV|SmartTvA)/([1-9]{1}(?:\.[0-9]{1}){1,2})')
10+
end
11+
712
def parse
813
return nil unless hbb_tv?
914

@@ -25,8 +30,7 @@ def parser_name
2530
end
2631

2732
def hbb_tv?
28-
regex = '(?:HbbTV|SmartTvA)/([1-9]{1}(?:\.[0-9]{1}){1,2})'
29-
match_user_agent(regex)
33+
match_user_agent_r(@hbb_regex)
3034
end
3135
end
3236
end

lib/device_detector/parser/device/notebook.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ class DeviceDetector
44
module Parser
55
module Device
66
class Notebook < AbstractDeviceParser
7+
def initialize
8+
super
9+
@notebook_regex = build_regex_for_ua('FBMD/')
10+
end
11+
712
def parse
8-
return nil unless match_user_agent('FBMD/')
13+
return nil unless match_user_agent_r(@notebook_regex)
914

1015
super
1116
end

lib/device_detector/parser/device/shell_tv.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ class DeviceDetector
44
module Parser
55
module Device
66
class ShellTv < AbstractDeviceParser
7+
def initialize
8+
super
9+
@shell_tv_regex = build_regex_for_ua('[a-z]+[ _]Shell[ _]\w{6}|tclwebkit(\d+[.\d]*)')
10+
end
11+
712
def parse
813
return nil unless shell_tv?
914

@@ -25,8 +30,7 @@ def parser_name
2530
end
2631

2732
def shell_tv?
28-
regex = '[a-z]+[ _]Shell[ _]\w{6}|tclwebkit(\d+[.\d]*)'
29-
match_user_agent(regex)
33+
match_user_agent_r(@shell_tv_regex)
3034
end
3135
end
3236
end

0 commit comments

Comments
 (0)