Skip to content

Commit cc0af33

Browse files
committed
Merge pull request #311 from bouk/fix-quadratic-performance-in-concat_javascript_sources
Fix quadratic performance in concat_javascript_sources
1 parent 04c8fb8 commit cc0af33

File tree

7 files changed

+90
-48
lines changed

7 files changed

+90
-48
lines changed

lib/sprockets/utils.rb

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,19 @@ def hash_reassoc(hash, *keys, &block)
7474
def string_end_with_semicolon?(str)
7575
i = str.size - 1
7676
while i >= 0
77-
c = str[i]
77+
c = str[i].ord
7878
i -= 1
79-
if c == "\n" || c == " " || c == "\t"
80-
next
81-
elsif c != ";"
82-
return false
83-
else
84-
return true
79+
80+
# Need to compare against the ordinals because the string can be UTF_8 or UTF_32LE encoded
81+
# 0x0A == "\n"
82+
# 0x20 == " "
83+
# 0x09 == "\t"
84+
# 0x3B == ";"
85+
unless c == 0x0A || c == 0x20 || c == 0x09
86+
return c === 0x3B
8587
end
8688
end
89+
8790
true
8891
end
8992

@@ -95,11 +98,21 @@ def string_end_with_semicolon?(str)
9598
#
9699
# Returns buf String.
97100
def concat_javascript_sources(buf, source)
98-
if buf.bytesize > 0
99-
buf << ";" unless string_end_with_semicolon?(buf)
100-
buf << "\n" unless buf.end_with?("\n")
101+
if source.bytesize > 0
102+
buf << source
103+
104+
# If the source contains non-ASCII characters, indexing on it becomes O(N).
105+
# This will lead to O(N^2) performance in string_end_with_semicolon?, so we should use 32 bit encoding to make sure indexing stays O(1)
106+
source = source.encode(Encoding::UTF_32LE) unless source.ascii_only?
107+
108+
if !string_end_with_semicolon?(source)
109+
buf << ";\n"
110+
elsif source[source.size - 1].ord != 0x0A
111+
buf << "\n"
112+
end
101113
end
102-
buf << source
114+
115+
buf
103116
end
104117

105118
# Internal: Prepends a leading "." to an extension if its missing.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
<%= resolve("foo.js", compat: true) %>,
2-
<%= resolve("foo.js", :content_type => "application/javascript", compat: true) %>,
1+
<%= resolve("foo.js", compat: true) %>;
2+
<%= resolve("foo.js", :content_type => "application/javascript", compat: true) %>;
33
<%= resolve("foo.js", :content_type => :self, compat: true) %>

test/test_asset.rb

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -125,26 +125,6 @@ def self.test(name, &block)
125125
end
126126
end
127127

128-
test "modify asset's dependency file" do
129-
main = fixture_path('asset/test-main.js.erb')
130-
dep = fixture_path('asset/test-dep.js')
131-
132-
sandbox main, dep do
133-
write(main, "//= depend_on test-dep\n<%= File.read('#{dep}') %>")
134-
write(dep, "a;")
135-
asset = asset('test-main.js')
136-
old_digest = asset.hexdigest
137-
old_uri = asset.uri
138-
assert_equal "a;", asset.to_s
139-
140-
write(dep, "b;")
141-
asset = asset('test-main.js')
142-
refute_equal old_digest, asset.hexdigest
143-
refute_equal old_uri, asset.uri
144-
assert_equal "b;", asset.to_s
145-
end
146-
end
147-
148128
test "remove asset's dependency file" do
149129
main = fixture_path('asset/test-main.js')
150130
dep = fixture_path('asset/test-dep.js')
@@ -420,6 +400,26 @@ def setup
420400

421401
include AssetTests
422402

403+
test "modify asset's dependency file" do
404+
main = fixture_path('asset/test-main.js.erb')
405+
dep = fixture_path('asset/test-dep.js')
406+
407+
sandbox main, dep do
408+
write(main, "//= depend_on test-dep\n<%= File.read('#{dep}') %>")
409+
write(dep, "a;")
410+
asset = asset('test-main.js')
411+
old_digest = asset.hexdigest
412+
old_uri = asset.uri
413+
assert_equal "a;", asset.to_s
414+
415+
write(dep, "b;")
416+
asset = asset('test-main.js')
417+
refute_equal old_digest, asset.hexdigest
418+
refute_equal old_uri, asset.uri
419+
assert_equal "b;", asset.to_s
420+
end
421+
end
422+
423423
test "uri" do
424424
assert_equal "file://#{fixture_path_for_uri('asset/application.js')}?type=application/javascript&pipeline=self&id=xxx",
425425
normalize_uri(@asset.uri)
@@ -508,6 +508,27 @@ def setup
508508

509509
include AssetTests
510510

511+
test "modify asset's dependency file" do
512+
main = fixture_path('asset/test-main.js.erb')
513+
dep = fixture_path('asset/test-dep.js')
514+
515+
sandbox main, dep do
516+
write(main, "//= depend_on test-dep\n<%= File.read('#{dep}') %>")
517+
write(dep, "a;")
518+
asset = asset('test-main.js')
519+
old_digest = asset.hexdigest
520+
old_uri = asset.uri
521+
assert_equal "a;\n", asset.to_s
522+
523+
write(dep, "b;")
524+
asset = asset('test-main.js')
525+
refute_equal old_digest, asset.hexdigest
526+
refute_equal old_uri, asset.uri
527+
assert_equal "b;\n", asset.to_s
528+
end
529+
end
530+
531+
511532
test "uri" do
512533
assert_equal "file://#{fixture_path_for_uri('asset/application.js')}?type=application/javascript&id=xxx",
513534
normalize_uri(@asset.uri)
@@ -1017,6 +1038,7 @@ def setup
10171038
define("application.js", "application-955b2dddd0d1449b1c617124b83b46300edadec06d561104f7f6165241b31a94.js")
10181039
define("application.css", "application-46d50149c56fc370805f53c29f79b89a52d4cc479eeebcdc8db84ab116d7ab1a.css")
10191040
define("POW.png", "POW-1da2e59df75d33d8b74c3d71feede698f203f136512cbaab20c68a5bdebd5800.png")
1041+
;
10201042
EOS
10211043
assert_equal [
10221044
"file://#{fixture_path_for_uri("asset/POW.png")}?type=image/png&id=xxx",
@@ -1035,6 +1057,7 @@ def setup
10351057
define("application.js", "application-955b2dddd0d1449b1c617124b83b46300edadec06d561104f7f6165241b31a94.js")
10361058
define("application.css", "application-46d50149c56fc370805f53c29f79b89a52d4cc479eeebcdc8db84ab116d7ab1a.css")
10371059
define("POW.png", "POW-1da2e59df75d33d8b74c3d71feede698f203f136512cbaab20c68a5bdebd5800.png")
1060+
;
10381061
EOS
10391062

10401063
assert_equal [
@@ -1189,7 +1212,7 @@ def setup
11891212
end
11901213

11911214
test "appends missing semicolons" do
1192-
assert_equal "var Bar\n;\n\n(function() {\n var Foo\n})\n",
1215+
assert_equal "var Bar\n;\n\n(function() {\n var Foo\n})\n;\n",
11931216
asset("semicolons.js").to_s
11941217
end
11951218

test/test_context.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ def datauri(path)
4949
end
5050

5151
test "resolve with content type" do
52-
assert_equal [fixture_path_for_uri("context/foo.js"),
53-
fixture_path_for_uri("context/foo.js"),
54-
fixture_path_for_uri("context/foo.js")
55-
].join(",\n"), @env["resolve_content_type.js"].to_s.strip
52+
assert_equal(<<-FILE, @env["resolve_content_type.js"].to_s)
53+
#{fixture_path_for_uri("context/foo.js")};
54+
#{fixture_path_for_uri("context/foo.js")};
55+
#{fixture_path_for_uri("context/foo.js")};
56+
FILE
5657
end
5758
end
5859

test/test_engines.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class TestEngines < Sprockets::TestCase
3030
env = new_environment
3131
env.register_engine ".alert", AlertProcessor
3232
asset = env["hello.alert"]
33-
assert_equal 'alert("Hello world!\n");', asset.to_s
33+
assert_equal "alert(\"Hello world!\\n\");\n", asset.to_s
3434
assert_equal 'application/javascript', asset.content_type
3535
end
3636

test/test_environment.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,7 @@ def foo; end
796796
assert processor = @env.preprocessors['application/javascript'][0]
797797
assert_kind_of Sprockets::DirectiveProcessor, processor
798798
@env.unregister_preprocessor('application/javascript', processor)
799-
assert_equal "// =require \"notfound\"\n", @env["missing_require.js"].to_s
799+
assert_equal "// =require \"notfound\"\n;\n", @env["missing_require.js"].to_s
800800
end
801801
end
802802

test/test_utils.rb

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,21 @@ def test_string_ends_with_semicolon
100100
end
101101

102102
def test_concat_javascript_sources
103-
assert_equal "var foo;\n", concat_javascript_sources("", "var foo;\n".freeze)
104-
assert_equal "\nvar foo;\n", concat_javascript_sources("\n", "var foo;\n".freeze)
105-
assert_equal " \nvar foo;\n", concat_javascript_sources(" ", "var foo;\n".freeze)
106-
107-
assert_equal "var foo;\nvar bar;\n", concat_javascript_sources("var foo;\n", "var bar;\n".freeze)
108-
assert_equal "var foo;\nvar bar", concat_javascript_sources("var foo", "var bar".freeze)
109-
assert_equal "var foo;\nvar bar;", concat_javascript_sources("var foo;", "var bar;".freeze)
110-
assert_equal "var foo;\nvar bar;", concat_javascript_sources("var foo", "var bar;".freeze)
103+
assert_equal "var foo;\n", apply_concat_javascript_sources("".freeze, "var foo;\n".freeze)
104+
assert_equal "\nvar foo;\n", apply_concat_javascript_sources("\n".freeze, "var foo;\n".freeze)
105+
assert_equal " \nvar foo;\n", apply_concat_javascript_sources(" ".freeze, "var foo;\n".freeze)
106+
107+
assert_equal "var foo;\nvar bar;\n", apply_concat_javascript_sources("var foo;\n".freeze, "var bar;\n".freeze)
108+
assert_equal "var foo;\nvar bar;\n", apply_concat_javascript_sources("var foo".freeze, "var bar".freeze)
109+
assert_equal "var foo;\nvar bar;\n", apply_concat_javascript_sources("var foo;".freeze, "var bar;".freeze)
110+
assert_equal "var foo;\nvar bar;\n", apply_concat_javascript_sources("var foo".freeze, "var bar;".freeze)
111111
end
112112

113+
def apply_concat_javascript_sources(*args)
114+
args.reduce(String.new(""), &method(:concat_javascript_sources))
115+
end
116+
117+
113118
def test_post_order_depth_first_search
114119
m = []
115120
m[11] = [4, 5, 10]

0 commit comments

Comments
 (0)