Skip to content

Commit 721877f

Browse files
rwstaunereregon
authored andcommitted
Optimize feature loading when require is called with absolute .rb path
If require is called with an absolute path like `/path/to/thing.rb` we can try that as is first rather than trying `.rb.rb` and `.rb.so` first. Add require specs for absolute paths: - with no extension (RB ext and C ext) - .so becomes .bundle on darwin
1 parent f1c7d51 commit 721877f

File tree

7 files changed

+66
-41
lines changed

7 files changed

+66
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Compatibility:
1616
Performance:
1717

1818
* Optimize calls with `ruby2_keywords` forwarding by deciding it per call site instead of per callee thanks to [my fix in CRuby 3.2](https://bugs.ruby-lang.org/issues/18625) (@eregon).
19+
* Optimize feature loading when require is called with an absolute path to a .rb file (@rwstauner).
1920

2021
Changes:
2122

spec/ruby/core/kernel/shared/load.rb

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
main = self
22

3+
# The big difference is Kernel#load does not attempt to add an extension to the passed path, unlike Kernel#require
34
describe :kernel_load, shared: true do
45
before :each do
56
CodeLoadingSpecs.spec_setup
@@ -10,22 +11,31 @@
1011
CodeLoadingSpecs.spec_cleanup
1112
end
1213

13-
it "loads a non-extensioned file as a Ruby source file" do
14-
path = File.expand_path "load_fixture", CODE_LOADING_DIR
15-
@object.load(path).should be_true
16-
ScratchPad.recorded.should == [:no_ext]
17-
end
14+
describe "(path resolution)" do
15+
# This behavior is specific to Kernel#load, it differs for Kernel#require
16+
it "loads a non-extensioned file as a Ruby source file" do
17+
path = File.expand_path "load_fixture", CODE_LOADING_DIR
18+
@object.load(path).should be_true
19+
ScratchPad.recorded.should == [:no_ext]
20+
end
1821

19-
it "loads a non .rb extensioned file as a Ruby source file" do
20-
path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR
21-
@object.load(path).should be_true
22-
ScratchPad.recorded.should == [:no_rb_ext]
23-
end
22+
it "loads a non .rb extensioned file as a Ruby source file" do
23+
path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR
24+
@object.load(path).should be_true
25+
ScratchPad.recorded.should == [:no_rb_ext]
26+
end
2427

25-
it "loads from the current working directory" do
26-
Dir.chdir CODE_LOADING_DIR do
27-
@object.load("load_fixture.rb").should be_true
28-
ScratchPad.recorded.should == [:loaded]
28+
it "loads from the current working directory" do
29+
Dir.chdir CODE_LOADING_DIR do
30+
@object.load("load_fixture.rb").should be_true
31+
ScratchPad.recorded.should == [:loaded]
32+
end
33+
end
34+
35+
# This behavior is specific to Kernel#load, it differs for Kernel#require
36+
it "does not look for a c-extension file when passed a path without extension (when no .rb is present)" do
37+
path = File.join CODE_LOADING_DIR, "a", "load_fixture"
38+
-> { @object.send(@method, path) }.should raise_error(LoadError)
2939
end
3040
end
3141

spec/ruby/core/kernel/shared/require.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,32 @@
212212

213213
describe :kernel_require, shared: true do
214214
describe "(path resolution)" do
215+
it "loads .rb file when passed absolute path without extension" do
216+
path = File.expand_path "load_fixture", CODE_LOADING_DIR
217+
@object.send(@method, path).should be_true
218+
# This should _not_ be [:no_ext]
219+
ScratchPad.recorded.should == [:loaded]
220+
end
221+
222+
platform_is :linux, :darwin do
223+
it "loads c-extension file when passed absolute path without extension when no .rb is present" do
224+
path = File.join CODE_LOADING_DIR, "a", "load_fixture"
225+
-> { @object.send(@method, path) }.should raise_error(Exception, /file too short/)
226+
end
227+
end
228+
229+
platform_is :darwin do
230+
it "loads .bundle file when passed absolute path with .so" do
231+
path = File.join CODE_LOADING_DIR, "a", "load_fixture.so"
232+
-> { @object.send(@method, path) }.should raise_error(Exception, /load_fixture\.bundle.+file too short/)
233+
end
234+
end
235+
236+
it "does not try an extra .rb if the path already ends in .rb" do
237+
path = File.join CODE_LOADING_DIR, "d", "load_fixture.rb"
238+
-> { @object.send(@method, path) }.should raise_error(LoadError)
239+
end
240+
215241
# For reference see [ruby-core:24155] in which matz confirms this feature is
216242
# intentional for security reasons.
217243
it "does not load a bare filename unless the current working directory is in $LOAD_PATH" do
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ScratchPad << :rbrb

spec/tags/core/kernel/require_tags.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ slow:Kernel#require (concurrently) blocks based on the path
22
slow:Kernel.require (concurrently) blocks based on the path
33
slow:Kernel#require ($LOADED_FEATURES) unicode_normalize is part of core and not $LOADED_FEATURES
44
slow:Kernel.require ($LOADED_FEATURES) unicode_normalize is part of core and not $LOADED_FEATURES
5-
fails:Kernel#require (non-extensioned path) loads a .rb extensioned file when a C-extension file exists on an earlier load path
6-
fails:Kernel#require (non-extensioned path) does not load a feature twice when $LOAD_PATH has been modified
75
slow:Kernel#require ($LOADED_FEATURES) complex, enumerator, rational, thread, ruby2_keywords are already required
86
slow:Kernel.require ($LOADED_FEATURES) complex, enumerator, rational, thread, ruby2_keywords are already required
97
slow:Kernel#require complex, enumerator, rational, thread, ruby2_keywords, fiber are already required

src/main/java/org/truffleruby/language/loader/FeatureLoader.java

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ private String basenameWithoutExtension(String path) {
179179
}
180180

181181
private boolean hasExtension(String path) {
182-
return path.endsWith(TruffleRuby.EXTENSION) || path.endsWith(RubyLanguage.CEXT_EXTENSION) ||
183-
path.endsWith(".so");
182+
return path.endsWith(TruffleRuby.EXTENSION) || path.endsWith(".so") ||
183+
(!Platform.CEXT_SUFFIX_IS_SO && path.endsWith(RubyLanguage.CEXT_EXTENSION));
184184
}
185185

186186
public void setWorkingDirectory(String cwd) {
@@ -314,6 +314,7 @@ private String findFeatureImpl(String feature) {
314314
if (feature.startsWith(RubyLanguage.RESOURCE_SCHEME) || new File(feature).isAbsolute()) {
315315
found = findFeatureWithAndWithoutExtension(feature);
316316
} else if (hasExtension(feature)) {
317+
String path = translateIfNativePath(feature);
317318
final RubyArray expandedLoadPath = (RubyArray) DispatchNode.getUncached().call(
318319
context.getCoreLibrary().truffleFeatureLoaderModule,
319320
"get_expanded_load_path");
@@ -324,7 +325,7 @@ private String findFeatureImpl(String feature) {
324325
RubyLanguage.LOGGER.info(String.format("from load path %s...", loadPath));
325326
}
326327

327-
String fileWithinPath = new File(loadPath, translateIfNativePath(feature)).getPath();
328+
String fileWithinPath = new File(loadPath, path).getPath();
328329
final String result = findFeatureWithExactPath(fileWithinPath);
329330

330331
if (result != null) {
@@ -368,41 +369,28 @@ private String findFeatureImpl(String feature) {
368369
}
369370

370371
private String translateIfNativePath(String feature) {
371-
if (!RubyLanguage.CEXT_EXTENSION.equals(".so") && feature.endsWith(".so")) {
372+
if (!Platform.CEXT_SUFFIX_IS_SO && feature.endsWith(".so")) {
372373
final String base = feature.substring(0, feature.length() - 3);
373374
return base + RubyLanguage.CEXT_EXTENSION;
374375
} else {
375376
return feature;
376377
}
377378
}
378379

380+
// Only used when the path is absolute
379381
private String findFeatureWithAndWithoutExtension(String path) {
380382
assert new File(path).isAbsolute();
381383

382-
if (path.endsWith(".so")) {
383-
final String pathWithNativeExt = translateIfNativePath(path);
384-
final String asCExt = findFeatureWithExactPath(pathWithNativeExt);
385-
if (asCExt != null) {
386-
return asCExt;
384+
if (hasExtension(path)) {
385+
return findFeatureWithExactPath(translateIfNativePath(path));
386+
} else {
387+
final String asRuby = findFeatureWithExactPath(path + TruffleRuby.EXTENSION);
388+
if (asRuby != null) {
389+
return asRuby;
387390
}
388-
}
389-
390-
final String asRuby = findFeatureWithExactPath(path + TruffleRuby.EXTENSION);
391-
if (asRuby != null) {
392-
return asRuby;
393-
}
394391

395-
final String asCExt = findFeatureWithExactPath(path + RubyLanguage.CEXT_EXTENSION);
396-
if (asCExt != null) {
397-
return asCExt;
392+
return findFeatureWithExactPath(path + RubyLanguage.CEXT_EXTENSION);
398393
}
399-
400-
final String withoutExtension = findFeatureWithExactPath(path);
401-
if (withoutExtension != null) {
402-
return withoutExtension;
403-
}
404-
405-
return null;
406394
}
407395

408396
private String findFeatureWithExactPath(String path) {

src/main/java/org/truffleruby/platform/Platform.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public abstract class Platform extends BasicPlatform {
3434

3535
public static final String LIB_SUFFIX = determineLibSuffix();
3636
public static final String CEXT_SUFFIX = OS == OS_TYPE.DARWIN ? ".bundle" : LIB_SUFFIX;
37+
public static final boolean CEXT_SUFFIX_IS_SO = CEXT_SUFFIX.equals(".so");
3738

3839
private static String determineLibSuffix() {
3940
switch (OS) {

0 commit comments

Comments
 (0)