Skip to content

Commit c95ee78

Browse files
tompngst0012
andauthored
Improve prompt generating performance by caching prompt parts(%m, %M) (#1127)
* 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. * Introduce `with_prompt_part_cahced do end` to enable prompt caching * Add prompt_part_cache nil-able reason comment Co-authored-by: Stan Lo <stan001212@gmail.com> --------- Co-authored-by: Stan Lo <stan001212@gmail.com>
1 parent 3893f18 commit c95ee78

File tree

3 files changed

+87
-16
lines changed

3 files changed

+87
-16
lines changed

lib/irb.rb

Lines changed: 36 additions & 16 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
@@ -238,13 +239,7 @@ def read_input(prompt)
238239
end
239240
end
240241

241-
def readmultiline
242-
prompt = generate_prompt([], false, 0)
243-
244-
# multiline
245-
return read_input(prompt) if @context.io.respond_to?(:check_termination)
246-
247-
# nomultiline
242+
def read_input_nomultiline(prompt)
248243
code = +''
249244
line_offset = 0
250245
loop do
@@ -265,6 +260,20 @@ def readmultiline
265260
end
266261
end
267262

263+
def readmultiline
264+
with_prompt_part_cached do
265+
prompt = generate_prompt([], false, 0)
266+
267+
if @context.io.respond_to?(:check_termination)
268+
# multiline
269+
read_input(prompt)
270+
else
271+
# nomultiline
272+
read_input_nomultiline(prompt)
273+
end
274+
end
275+
end
276+
268277
def each_top_level_statement
269278
loop do
270279
code = readmultiline
@@ -567,6 +576,13 @@ def inspect
567576

568577
private
569578

579+
def with_prompt_part_cached
580+
@prompt_part_cache = {}
581+
yield
582+
ensure
583+
@prompt_part_cache = nil
584+
end
585+
570586
def generate_prompt(opens, continue, line_offset)
571587
ltype = @scanner.ltype_from_open_tokens(opens)
572588
indent = @scanner.calc_indent_level(opens)
@@ -598,25 +614,29 @@ def generate_prompt(opens, continue, line_offset)
598614
end
599615

600616
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
617+
if str.size > PROMPT_MAIN_TRUNCATE_LENGTH
618+
str = str[0, PROMPT_MAIN_TRUNCATE_LENGTH - PROMPT_MAIN_TRUNCATE_OMISSION.size] + PROMPT_MAIN_TRUNCATE_OMISSION
606619
end
620+
str.tr(CONTROL_CHARACTERS_PATTERN, ' ')
607621
end
608622

609623
def format_prompt(format, ltype, indent, line_no) # :nodoc:
624+
# @prompt_part_cache could be nil in unit tests
625+
part_cache = @prompt_part_cache || {}
610626
format.gsub(/%([0-9]+)?([a-zA-Z%])/) do
611627
case $2
612628
when "N"
613629
@context.irb_name
614630
when "m"
615-
main_str = "#{@context.safe_method_call_on_main(:to_s)}" rescue "!#{$!.class}"
616-
truncate_prompt_main(main_str)
631+
part_cache[:m] ||= (
632+
main_str = "#{@context.safe_method_call_on_main(:to_s)}" rescue "!#{$!.class}"
633+
truncate_prompt_main(main_str)
634+
)
617635
when "M"
618-
main_str = "#{@context.safe_method_call_on_main(:inspect)}" rescue "!#{$!.class}"
619-
truncate_prompt_main(main_str)
636+
part_cache[:M] ||= (
637+
main_str = "#{@context.safe_method_call_on_main(:inspect)}" rescue "!#{$!.class}"
638+
truncate_prompt_main(main_str)
639+
)
620640
when "l"
621641
ltype
622642
when "i"

test/irb/test_context.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,27 @@ 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+
prompt1, prompt2 = nil
656+
irb.send(:with_prompt_part_cached) do
657+
prompt1 = irb.send(:format_prompt, format, nil, 1, 1)
658+
prompt2 = irb.send(:format_prompt, format, nil, 1, 1)
659+
end
660+
assert_equal(prompt1, prompt2)
661+
assert_match(pattern, prompt1)
662+
663+
prompt3 = irb.send(:format_prompt, format, nil, 1, 1)
664+
assert_not_equal(prompt1, prompt3)
665+
assert_match(pattern, prompt3)
666+
end
667+
647668
def test_prompt_main_truncate
648669
main = Struct.new(:to_s).new("a" * 100)
649670
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_main_to_s_call_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)