Skip to content

Commit fdea403

Browse files
committed
Improve prompt generating performance by caching prompt parts(%m, %M)
In prompt calculation, `main.to_s` was called on every keystroke and every line in multiline input. Cache prompt parts(%m, %M) so that `main.to_s` is only called once per read-eval cycle.
1 parent 3893f18 commit fdea403

3 files changed

Lines changed: 66 additions & 9 deletions

File tree

lib/irb.rb

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class Irb
8787
# Creates a new irb session
8888
def initialize(workspace = nil, input_method = nil, from_binding: false)
8989
@from_binding = from_binding
90+
@prompt_part_cache = nil
9091
@context = Context.new(self, workspace, input_method)
9192
@context.workspace.load_helper_methods_to_main
9293
@signal_status = :IN_IRB
@@ -239,6 +240,7 @@ def read_input(prompt)
239240
end
240241

241242
def readmultiline
243+
@prompt_part_cache = {}
242244
prompt = generate_prompt([], false, 0)
243245

244246
# multiline
@@ -263,6 +265,8 @@ def readmultiline
263265
continue = @scanner.should_continue?(tokens)
264266
prompt = generate_prompt(opens, continue, line_offset)
265267
end
268+
ensure
269+
@prompt_part_cache = nil
266270
end
267271

268272
def each_top_level_statement
@@ -598,25 +602,28 @@ def generate_prompt(opens, continue, line_offset)
598602
end
599603

600604
def truncate_prompt_main(str) # :nodoc:
601-
str = str.tr(CONTROL_CHARACTERS_PATTERN, ' ')
602-
if str.size <= PROMPT_MAIN_TRUNCATE_LENGTH
603-
str
604-
else
605-
str[0, PROMPT_MAIN_TRUNCATE_LENGTH - PROMPT_MAIN_TRUNCATE_OMISSION.size] + PROMPT_MAIN_TRUNCATE_OMISSION
605+
if str.size > PROMPT_MAIN_TRUNCATE_LENGTH
606+
str = str[0, PROMPT_MAIN_TRUNCATE_LENGTH - PROMPT_MAIN_TRUNCATE_OMISSION.size] + PROMPT_MAIN_TRUNCATE_OMISSION
606607
end
608+
str.tr(CONTROL_CHARACTERS_PATTERN, ' ')
607609
end
608610

609611
def format_prompt(format, ltype, indent, line_no) # :nodoc:
612+
part_cache = @prompt_part_cache || {}
610613
format.gsub(/%([0-9]+)?([a-zA-Z%])/) do
611614
case $2
612615
when "N"
613616
@context.irb_name
614617
when "m"
615-
main_str = "#{@context.safe_method_call_on_main(:to_s)}" rescue "!#{$!.class}"
616-
truncate_prompt_main(main_str)
618+
part_cache[:m] ||= (
619+
main_str = "#{@context.safe_method_call_on_main(:to_s)}" rescue "!#{$!.class}"
620+
truncate_prompt_main(main_str)
621+
)
617622
when "M"
618-
main_str = "#{@context.safe_method_call_on_main(:inspect)}" rescue "!#{$!.class}"
619-
truncate_prompt_main(main_str)
623+
part_cache[:M] ||= (
624+
main_str = "#{@context.safe_method_call_on_main(:inspect)}" rescue "!#{$!.class}"
625+
truncate_prompt_main(main_str)
626+
)
620627
when "l"
621628
ltype
622629
when "i"

test/irb/test_context.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,26 @@ def test_prompt_main_inspect_escape
644644
assert_equal("irb(main\\n main)>", irb.send(:format_prompt, 'irb(%M)>', nil, 1, 1))
645645
end
646646

647+
def test_prompt_part_cached
648+
main = Object.new
649+
def main.to_s; "to_s#{rand}"; end
650+
def main.inspect; "inspect#{rand}"; end
651+
irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new)
652+
format = '[%m %M %m %M]>'
653+
pattern = /\A\[(to_s[\d.]+) (inspect[\d.]+) \1 \2\]>\z/
654+
655+
irb.instance_variable_set(:@prompt_part_cache, {})
656+
prompt1 = irb.send(:format_prompt, format, nil, 1, 1)
657+
prompt2 = irb.send(:format_prompt, format, nil, 1, 1)
658+
assert_equal(prompt1, prompt2)
659+
assert_match(pattern, prompt1)
660+
661+
irb.instance_variable_set(:@prompt_part_cache, nil)
662+
prompt3 = irb.send(:format_prompt, format, nil, 1, 1)
663+
assert_not_equal(prompt1, prompt3)
664+
assert_match(pattern, prompt3)
665+
end
666+
647667
def test_prompt_main_truncate
648668
main = Struct.new(:to_s).new("a" * 100)
649669
def main.inspect; to_s.inspect; end

test/irb/yamatanooroti/test_rendering.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,36 @@ def test_nomultiline
112112
close
113113
end
114114

115+
def test_prompt_main_part_cached
116+
start_terminal(25, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: /irb\(main\)/)
117+
write(<<~'EOC')
118+
@count = 0
119+
def self.to_s; @count += 1; "[#{@count}]"; end
120+
if false
121+
123
122+
end
123+
if false
124+
123
125+
end
126+
EOC
127+
assert_screen(<<~'EOC')
128+
irb(main):001> @count = 0
129+
=> 0
130+
irb(main):002> def self.to_s; @count += 1; "[#{@count}]"; end
131+
=> :to_s
132+
irb([1]):003* if false
133+
irb([1]):004* 123
134+
irb([1]):005> end
135+
=> nil
136+
irb([2]):006* if false
137+
irb([2]):007* 123
138+
irb([2]):008> end
139+
=> nil
140+
irb([3]):009>
141+
EOC
142+
close
143+
end
144+
115145
def test_multiline_paste
116146
start_terminal(25, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: /irb\(main\)/)
117147
write(<<~EOC)

0 commit comments

Comments
 (0)