Skip to content

inspect memory limit fix #100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jul 25, 2017
Merged

inspect memory limit fix #100

merged 16 commits into from
Jul 25, 2017

Conversation

viuginick1
Copy link
Collaborator

Sets a memory limit for execution of inspect (default 10mb)

@valich valich self-assigned this Jul 4, 2017
@valich valich self-requested a review July 4, 2017 14:11
Copy link
Contributor

@valich valich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some (mostly) code style problems must be addressed before merging

bin/rdebug-ide Outdated
@@ -45,6 +51,7 @@ EOB
options.evaluation_timeout = timeout
end
opts.on('--stop', 'stop when the script is loaded') {options.stop = true}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change needed here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

bin/rdebug-ide Outdated
@@ -154,4 +161,4 @@ if options.attach_mode
end
else
Debugger.debug_program(options)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@@ -380,9 +384,37 @@ def max_compact_name_size
50
end

def inspect_with_allocation_control(slice, memory_limit)
x = Thread.current
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we conceive a better name for this variable? Like current_thread, start_thread or alike

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

trace = TracePoint.new(:c_call, :call) do |tp|
curr_alloc_size = ObjectSpace.memsize_of_all

if(curr_alloc_size - start_alloc_size > 1e6*memory_limit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

end
end

trace.enable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to use TracePoint#enable( &block) here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TracePoint#enable( &block) works differently

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please elaborate more?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a block is given, the trace will only be enabled within the scope of the block.

Copy link
Collaborator Author

@viuginick1 viuginick1 Jul 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my implementation, the checks are carried out more often, which means that the probability of detecting an overflow increases

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so I suggest changing these three lines with

trace.enable { slice.inspect }

In my implementation, the checks are carried out more often, which means that the probability of detecting an overflow increases

Sorry, did not get what overflow you're talking about.

slice.inspect
trace.disable
rescue MemoryLimitError
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it makes sense to add some logging for such cases? Or return something very special

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added logging

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think printing also a stacktrace of an exception may help investigating the problems in inspect calls for users.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This backtrack will end with the calls of our debase and r-d-i methods. I think that this is not very useful.

def compact_array_str(value)
slice = value[0..10]
compact = slice.inspect

if (defined?(JRUBY_VERSION) || ENV['DEBUGGER_MEMORY_LIMIT'].to_i <= 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be replaced with compact = if (...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think that this is applicable in this case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@@ -425,4 +457,4 @@ def build_value_attr(escaped_value_str)

end

end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@viuginick1 viuginick1 force-pushed the upstream branch 2 times, most recently from a6dff63 to 9665253 Compare July 5, 2017 12:09
@@ -118,7 +118,7 @@ def timeout(sec)
def debug_eval(str, b = get_binding)
begin
str = str.to_s
str.force_encoding('UTF-8') if(RUBY_VERSION > 2.0)
str.force_encoding('UTF-8') if(RUBY_VERSION > '2.0')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this relate to the logging changes?

@@ -45,6 +45,7 @@ def initialize(interface)
end

def print_msg(*args)
puts 'def print_msg(*args)'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this puts needed?

@@ -413,7 +415,7 @@ def compact_array_str(value)
compact = if (defined?(JRUBY_VERSION) || ENV['DEBUGGER_MEMORY_LIMIT'].to_i <= 0)
slice.inspect
else
compact = inspect_with_allocation_control(slice, ENV['DEBUGGER_MEMORY_LIMIT'].to_i)
compact = inspect_with_allocation_control(slice, ENV['DEBUGGER_MEMORY_LIMIT'].to_i, value.object_id.to_s)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure object_id us really useful for logging purposes: it does not tell anything about the object. I think the previous message already a good start.

@@ -404,6 +405,7 @@ def inspect_with_allocation_control(slice, memory_limit)
trace.disable
result
rescue MemoryLimitError => e
print_msg(e.message)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one may make use of --debug option here:

opts.on("-d", "--debug", "Debug self - prints information for debugging ruby-debug itself") do
     Debugger.cli_debug = true
     options.cli_debug = true
   end

bin/rdebug-ide Outdated
@@ -37,8 +37,8 @@ EOB
opts.separator ""
opts.separator "Options:"

ENV['DEBUGGER_MEMORY_LIMIT'] = '10'
opts.on("-m", "--memory-limit LIMIT", Integer, "evaluation memory limit in mb (default: 10)") do |limit|
ENV['DEBUGGER_MEMORY_LIMIT'] = '1'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What stands behind such defaults change? Could you please reason it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With 10 you have to wait for a pretty long time

curr_thread.raise MemoryLimitError, "Out of memory: evaluation of inspect took more than #{memory_limit}mb." if curr_thread.alive?
end
if Thread.respond_to?(:critical) and Thread.critical
raise ThreadError, "memory_limit within critical session"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU the execution of that line does not mean memory limit is exceeded. What does this check?

start_alloc_size = ObjectSpace.memsize_of_all
trace = TracePoint.new(:c_call, :call) do |tp|
curr_alloc_size = ObjectSpace.memsize_of_all
start_alloc_size = curr_alloc_size if (curr_alloc_size < start_alloc_size)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case of GC?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exactly

result = nil
inspect_thread = DebugThread.start {
start_alloc_size = ObjectSpace.memsize_of_all
trace = TracePoint.new(:c_call, :call) do |tp|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need trace variable? I don't see any usages..

Copy link
Collaborator Author

@viuginick1 viuginick1 Jul 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll disable it through 'trace'


if(curr_alloc_size - start_alloc_size > 1e6 * memory_limit)
curr_thread.raise MemoryLimitError, "Out of memory: evaluation of inspect took more than #{memory_limit}mb. \n#{caller.map{|l| "\t#{l}"}.join("\n")}"
inspect_thread.kill
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather move this kill to line 410, after join. It seems strange to me to kill the thread from this thread itself.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@@ -389,9 +392,19 @@ def inspect_with_allocation_control(slice, memory_limit)
result = nil
inspect_thread = DebugThread.start {
start_alloc_size = ObjectSpace.memsize_of_all
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks nice. One last question: do you think such checks on every call will slow down the execution?
The possible solution there is to make these checks only in 10% of calls (like Math.random > 0.9) or alike

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most often 'inspect' works fast and requires little memory, so in general the performance should not slow down. And in case of heavy inspect-s, when using probability, everything can crush with a high probability. Then why was this fix at all?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, it is worth running a :call and :c_call handler a little less often. Every fifth, for example.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, it is worth running a :call and :c_call handler a little less often. Every fifth, for example.

That's what I'm talking about.

inspect often works fast, but

  1. We add constant overhead on each call which may slow execution down in dozens of times;
  2. There are many inspects during variables collection so in total this matters

Copy link
Collaborator Author

@viuginick1 viuginick1 Jul 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still getting an INTERNAL ERROR (pipe or something) when using probabilistic scheme. Apparently, it is better to abandon this proposal.
https://pastebin.com/SL8R1DGc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show the snippet you're using to achieve the result?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

inspect_thread = DebugThread.start {
start_alloc_size = ObjectSpace.memsize_of_all
start_time = Time.now
max_time = Debugger.evaluation_timeout
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 10s evaluation timeout is too much for this place. The reason is that evaluation is an explicit user action so it's OK to try to wait for a possibly long evaluation. In the case of convenience infos calculated dozens of times per breakpoint, 10s does not make much sense to me. I'd say we should make it somewhere between 100 and 500ms.

Copy link
Contributor

@valich valich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor changes still needed

@@ -144,7 +158,46 @@ def print_string(string)
print_variable('encoding', string.encoding, 'instance') if string.respond_to?('encoding')
end
end


def exec_with_allocation_control(value, memory_limit, time_limit, exec_method, flag)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does flag mean? Not a helpful name for a parameter

attr_reader :message
attr_reader :backtrace

def initialize(message, backtrace)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one does not provide default backtrace value and the one below does. Is that intentional?

rescue MemoryLimitError, TimeLimitError => e
print_debug(e.message + "\n" + e.backtrace)

return nil if flag
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ternary

@valich valich merged commit 03a6101 into ruby-debug:master Jul 25, 2017
@valich
Copy link
Contributor

valich commented Jul 25, 2017

Squashed and merged into master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants