Skip to content

Conversation

@josevalim
Copy link
Member

@josevalim josevalim commented Jul 7, 2017

The main interface is a IEx.break!/4 function that sets up a breakpoint in module, function and arity with the given number of stops.

This function will instrument the given module and load a new version in memory with breakpoints at the given function and arity. If the module is recompiled, all breakpoints are lost.

When a breakpoint is reached, IEx will ask if you want to pry the given function and arity. In other words, this works similar to IEx.pry/0 as the running process becomes the evaluator of IEx commands and is temporarily changed to have a custom group leader. However, differently from IEx.pry/0, aliases and imports from the source code won't be available in the shell.

IEx helpers includes many conveniences related to breakpoints. Below they are listed with the full module, such as IEx.Helpers.breaks/0, but remember it can be called directly as breaks() inside IEx. They are:

  • break!/2 - sets up a breakpoint for a given Mod.fun/arity
  • break!/4 - sets up a breakpoint for the given module, function, arity
  • breaks/0 - prints all breakpoints and their ids
  • continue/0 - continues until the next breakpoint in the same shell
  • open/0 - opens editor on the current breakpoint
  • remove_breaks/0 - removes all breakpoints in all modules
  • remove_breaks/1 - removes all breakpoints in a given module
  • reset_break/1 - sets the number of stops on the given id to zero
  • reset_break/3 - sets the number of stops on the given module, function, arity to zero
  • respawn/0 - starts a new shell (breakpoints will ask for permission once more)
  • whereami/1 - shows the current location

By default, the number of stops in a breakpoint is 1. Any follow-up call won't stop the code execution unless another breakpoint is set.

Alternatively, the number of be increased by passing the stops argument. IEx.Helpers.reset_break/1 and IEx.Helpers.reset_break/3 can be used to reset the number back to zero. Note the module remains "instrumented" even after all stops on all breakpoints are consumed. You can remove the instrumentation in a given module by calling IEx.Helpers.remove_breaks/1 and on all modules by calling IEx.Helpers.remove_breaks/0.

To exit a breakpoint, the developer can either invoke continue(), which will block the shell until the next breakpoint is found or the process terminates, or invoke respawn(), which starts a new IEx shell, freeing up the pried one.

This functionality only works on Elixir code and requires OTP 20+.

Examples

The following sets up a breakpoint on URI.decode_query/2:

IEx.break!(URI, :decode_query, 2)

The following call will setup a breakpoint that stops once.

To set a breakpoint that will stop 10 times:

IEx.break!(URI, :decode_query, 10)

IEx.break!/2 is a convenience macro that allows breakpoints to be given in the Mod.fun/arity format:

require IEx
IEx.break!(URI.decode_query/2)

Or to set a breakpoint that will stop 10 times:

IEx.break!(URI.decode_query/2, 10)

This function returns the breakpoint ID and will raise if there is an error setting up the breakpoint.

Breaks and mix test

To use IEx.break!/4 during tests, you need to run Mix inside iex and pass the --trace to mix test to avoid running into timeouts:

iex -S mix test --trace
iex -S mix test path/to/file:line --trace

@josevalim josevalim force-pushed the jv-pry-breakpoint branch 3 times, most recently from 230680d to 7ad8d4f Compare July 7, 2017 16:50
@josevalim josevalim force-pushed the jv-pry-breakpoint branch from 7ad8d4f to a19ac52 Compare July 7, 2017 18:21
@josevalim
Copy link
Member Author

whereami() in action:

screen shot 2017-07-07 at 16 00 51

@josevalim josevalim force-pushed the jv-pry-breakpoint branch 3 times, most recently from d6c46d6 to cfede2f Compare July 8, 2017 12:08
@josevalim
Copy link
Member Author

open/0 and open/1 has landed.

@eksperimental
Copy link
Contributor

I guess we are trying to be consistent with whereis but I think we shoulnd't introduce more inconsistencies and whereami should be renamed where_am_i

@josevalim
Copy link
Member Author

josevalim commented Jul 8, 2017 via email

@josevalim josevalim force-pushed the jv-pry-breakpoint branch from cfede2f to b89ea99 Compare July 8, 2017 13:56
@slashmili
Copy link

slashmili commented Jul 8, 2017

GDB is using list

(gdb) break 15
Breakpoint 1 at 0x40069a: file factorial.c, line 15.
(gdb) run
Breakpoint 1, duplicate (x=32767, y=1) at factorial.c:15
15	        return x * y;
(gdb) list
10	        j = duplicate(j, i);
11	    printf("The factorial of %d is %d\n",num,j);
12	}
13
14	int duplicate(int x, int y) {
15	        return x * y;
16	}

Python debugger is using list

-> duplicate(1, 2)
(Pdb) list
  1  	def duplicate(x, y):
  2  	    return x * y
  3
  4
  5  	import pdb; pdb.set_trace()
  6  ->	duplicate(1, 2)

And Perl debugger is using l :

  DB<2> l
1==>	$i = 1;
2:	$j = 5;
3
4
5:	$result = $i * $j;

@josevalim
Copy link
Member Author

Thank you @slashmili! I think list is a bit overloaded for us though. Do you have any other suggestions?

@josevalim josevalim force-pushed the jv-pry-breakpoint branch from 3bfd67a to c9d2a23 Compare July 8, 2017 22:01
Copy link
Member

Choose a reason for hiding this comment

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

Isn't the tuple 3 elements?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, thank you!

@josevalim josevalim force-pushed the jv-pry-breakpoint branch 4 times, most recently from ac22534 to 9e28e66 Compare July 9, 2017 20:37
@josevalim josevalim force-pushed the jv-pry-breakpoint branch 2 times, most recently from a1aca33 to 29c2503 Compare July 9, 2017 23:35
@josevalim
Copy link
Member Author

Break/continue workflow on a function with breakpoint being invoked multiple times:

screen shot 2017-07-10 at 11 05 35

@josevalim josevalim force-pushed the jv-pry-breakpoint branch from 29c2503 to ca02358 Compare July 10, 2017 09:25
Copy link
Member

Choose a reason for hiding this comment

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

This is kind of hacky, isn't it? It's just to get around the foo warning. It also makes the regular shell and shell-in-pry behave differently for code like this.

Copy link
Member Author

Choose a reason for hiding this comment

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

The implementation is sound but the idea is debatable. I have tried the following approaches:

  1. Autocompletion of nullary calls - it is a bad user experience because it works only some times (for example, it wouldn't work for open/0 and whereami/0 because we also have other arities)
  2. Parens autocompletion of all calls - if we add only (, we get incomplete input most of the times. If we add both, then we need to go back one position (currently it is not possible to add both and then put the cursor on the middle)
  3. Accept continue in the shell - the UX is great, the inconsistency is, well, inconsistent

We can bring the warning back once we figure out autocompletion but since this feature is about the user experience, I have put it first here.

Copy link
Member

Choose a reason for hiding this comment

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

Given this section, should we talk somewhere about interrupt from the Ctrl-g menu? It's more powerful in a way it can solve this situation, but also infinite loops & waiting in a receive without a message.

Copy link
Member

Choose a reason for hiding this comment

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

Scrap that, I just read this bit further

@josevalim josevalim force-pushed the jv-pry-breakpoint branch from ca02358 to a85748a Compare July 10, 2017 09:40
Copy link
Member

Choose a reason for hiding this comment

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

Do we have a hash of the file stored somewhere or something similar? My experience with that kind of source printing is that if the source changes, something completely irrelevant and extremely confusing can be printed. If we had a hash we could display some kind of warning that file changed on disc compared to what we have compiled.

Copy link
Member Author

Choose a reason for hiding this comment

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

Great idea. There is no hashing though. But something we could do for now is to print mod.fun/arity we are debugging. That will help with making it clearer in many cases.

Copy link
Member Author

Choose a reason for hiding this comment

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

We do have the module version but that is built on the AST.

@josevalim josevalim force-pushed the jv-pry-breakpoint branch from a85748a to 9ea814f Compare July 10, 2017 09:57
@alexrp
Copy link

alexrp commented Jul 10, 2017

For what it's worth, in sdb, I went with source for displaying the current source context, e.g. source for 10 lines of context on both sides, or source 10 5 for 10 lines before the current line + 5 lines after the current line, etc.

@josevalim josevalim force-pushed the jv-pry-breakpoint branch from 88625ff to b11df4d Compare July 10, 2017 11:14
private functions of the module being pried. Module functions
still need to be accessed via `Mod.fun(args)`.
Alternatively, you can use `IEx.break!/4` to setup a breakpoint
Copy link
Member

Choose a reason for hiding this comment

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

Does it make sense to be more concise here and forward to the documentation of IEx.break!/4? So that we don't have to repeat the warning for OTP 20+ and all.

Copy link
Member Author

Choose a reason for hiding this comment

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

I just wanted to give a quick run down of the pros and cons so the user doesn't have to figure it out for themselves.

Copy link
Member

Choose a reason for hiding this comment

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

Is this really an ArgumentError? It feels like most of the errors above are not, for example the :otp_20_is_required. What if we introduce a IEx.BreakError or something similar?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call. I will go with runtime error.

Copy link
Member

Choose a reason for hiding this comment

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

Nitpicky nitpick but I guess we tend to go with no [] on last argument kw list:

Agent.start_link(__MODULE__, :handle_init, [], name: @agent)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, good call.

put_history(state)
handle_eval(Code.string_to_quoted(code, [line: line, file: "iex"]), code, line, iex_state, state)
put_whereami(state)
quoted = Code.string_to_quoted(code, [line: line, file: "iex"])
Copy link
Member

Choose a reason for hiding this comment

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

Would say same here for [] with options.

@doc """
Opens the current prying location.
This command only works inside a pry session started manually
Copy link
Member

Choose a reason for hiding this comment

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

IEx.pry/0, otherwise no linking?

# In both cases, we are safe to print and the request will
# suceed.
request =
case IEx.Server.evaluator do
Copy link
Member

Choose a reason for hiding this comment

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

IEx.Server.evaluator()?

end

def handle_call(:remove_breaks, _from, _counter) do
# Make sure to deinstrumented before clearing
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick but "deinstrumented" -> "deinstrument".

@table
|> :ets.match({:_, :"$1", :_, :_})
|> List.flatten
|> Enum.uniq
Copy link
Member

Choose a reason for hiding this comment

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

Bunch of parens for this calls?

{:dictionary, dictionary} = Process.info(pid, :dictionary)
dictionary[:evaluator]
{dictionary[:evaluator], pid}
end
Copy link
Member

Choose a reason for hiding this comment

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

Can maybe be refactored as

if pid = IEx.Server.local() do
  ...
else
  nil
end

but I don't have preference, just wanted to mention it.

self_leader = Process.group_leader
evaluator = opts[:evaluator] ||
:proc_lib.start(IEx.Evaluator, :init, [:ack, self_pid, self_leader, opts])
:proc_lib.start(IEx.Evaluator, :init, [:ack, self(), Process.group_leader, opts])
Copy link
Member

Choose a reason for hiding this comment

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

() as well I guess 😄

@josevalim josevalim force-pushed the jv-pry-breakpoint branch from 3e89dd3 to f3c826d Compare July 10, 2017 12:38
@josevalim josevalim merged commit 06f2146 into master Jul 10, 2017
@josevalim josevalim deleted the jv-pry-breakpoint branch July 10, 2017 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

7 participants