Skip to content
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

Add optional debug info to ophydobj with inspect #961

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

mrakitin
Copy link
Member

I am looking for a quick way to extract the information from what file an ophyd object was instantiated, and where its source code is. It can be done with inspect.stack(). It causes performance issues within __init__subclass__, so I decided to make it optional, enabled via an environment variable OPHYD_DEBUG_WITH_INSPECT. That can be useful for debugging.

Usage:

$ OPHYD_DEBUG_WITH_INSPECT=1 ipython --profile=collection --IPCompleter.use_jedi=False
...
In [1]: my_dev._stack_init
Out[1]:
[FrameInfo(frame=<frame at 0x7f48a8839050, file '/nsls2/users/mrakitin/src/ophyd/ophyd/ophydobj.py', line 189, code __init__>, filename='/nsls2/users/mrakitin/src/ophyd/ophyd/ophydobj.py', lineno=147, function='__init__', code_context=['            self._stack_init = inspect.stack()\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48a8aad5c0, file '/nsls2/users/mrakitin/src/ophyd/ophyd/device.py', line 414, code __init__>, filename='/nsls2/users/mrakitin/src/ophyd/ophyd/device.py', lineno=414, function='__init__', code_context=['        super().__init__(*args, **kwargs)\n'], index=0),
 FrameInfo(frame=<frame at 0x561316695e50, file '/nsls2/users/mrakitin/src/ophyd/ophyd/device.py', line 759, code __init__>, filename='/nsls2/users/mrakitin/src/ophyd/ophyd/device.py', lineno=725, function='__init__', code_context=['        super().__init__(name=name, parent=parent, kind=kind, **kwargs)\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48a8825c20, file '/nsls2/users/mrakitin/.ipython/profile_collection/startup/01-devices.py', line 3, code <module>>, filename='/nsls2/users/mrakitin/.ipython/profile_collection/startup/01-devices.py', lineno=3, function='<module>', code_context=["my_dev = MyDev('', name='my_dev')\n"], index=0),
 FrameInfo(frame=<frame at 0x7f48a8879c50, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/utils/py3compat.py', line 168, code execfile>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/utils/py3compat.py', lineno=168, function='execfile', code_context=["        exec(compiler(f.read(), fname, 'exec'), glob, loc)\n"], index=0),
 FrameInfo(frame=<frame at 0x5613156e9990, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/interactiveshell.py', line 2762, code safe_execfile>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/interactiveshell.py', lineno=2742, function='safe_execfile', code_context=['                    self.compile if shell_futures else None)\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48a8879a50, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', line 382, code _exec_file>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', lineno=380, function='_exec_file', code_context=['                                                 raise_exceptions=True)\n'], index=0),
 FrameInfo(frame=<frame at 0x5613157b1a20, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', line 412, code _run_startup_files>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', lineno=409, function='_run_startup_files', code_context=['                self._exec_file(fname)\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48de45a810, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', line 333, code init_code>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', lineno=318, function='init_code', code_context=['        self._run_startup_files()\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48dee48210, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/terminal/ipapp.py', line 323, code initialize>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/terminal/ipapp.py', lineno=323, function='initialize', code_context=['        self.init_code()\n'], index=0),
 FrameInfo(frame=<frame at 0x5613153d4370, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/traitlets/config/application.py', line 87, code catch_config_error>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/traitlets/config/application.py', lineno=87, function='catch_config_error', code_context=['        return method(app, *args, **kwargs)\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48dee1fad0, file '<decorator-gen-113>', line 2, code initialize>, filename='<decorator-gen-113>', lineno=2, function='initialize', code_context=None, index=None),
 FrameInfo(frame=<frame at 0x56131559a2f0, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/traitlets/config/application.py', line 664, code launch_instance>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/traitlets/config/application.py', lineno=663, function='launch_instance', code_context=['        app.initialize(argv)\n'], index=0),
 FrameInfo(frame=<frame at 0x5613156a15f0, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/__init__.py', line 126, code start_ipython>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/__init__.py', lineno=126, function='start_ipython', code_context=['    return launch_new_instance(argv=argv, **kwargs)\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48e09cc450, file '/opt/conda_envs/collection-2021-1.0/bin/ipython', line 11, code <module>>, filename='/opt/conda_envs/collection-2021-1.0/bin/ipython', lineno=11, function='<module>', code_context=['    sys.exit(start_ipython())\n'], index=0)]

In [2]: my_dev._stack_subclass
Out[2]:
[FrameInfo(frame=<frame at 0x561316695880, file '/nsls2/users/mrakitin/src/ophyd/ophyd/ophydobj.py', line 241, code __init_subclass__>, filename='/nsls2/users/mrakitin/src/ophyd/ophyd/ophydobj.py', lineno=232, function='__init_subclass__', code_context=['            cls._stack_subclass = inspect.stack()\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48a8833050, file '/nsls2/users/mrakitin/src/ophyd/ophyd/device.py', line 839, code __init_subclass__>, filename='/nsls2/users/mrakitin/src/ophyd/ophyd/device.py', lineno=838, function='__init_subclass__', code_context=['        super().__init_subclass__(**kwargs)\n'], index=0),
 FrameInfo(frame=<frame at 0x5613157ae7e0, file '/nsls2/users/mrakitin/.ipython/profile_collection/startup/00-base.py', line 6, code <module>>, filename='/nsls2/users/mrakitin/.ipython/profile_collection/startup/00-base.py', lineno=6, function='<module>', code_context=['class MyDev(Device):\n'], index=0),
 FrameInfo(frame=<frame at 0x5613157c7f60, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/utils/py3compat.py', line 168, code execfile>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/utils/py3compat.py', lineno=168, function='execfile', code_context=["        exec(compiler(f.read(), fname, 'exec'), glob, loc)\n"], index=0),
 FrameInfo(frame=<frame at 0x5613157a9850, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/interactiveshell.py', line 2762, code safe_execfile>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/interactiveshell.py', lineno=2742, function='safe_execfile', code_context=['                    self.compile if shell_futures else None)\n'], index=0),
 FrameInfo(frame=<frame at 0x5613156ffad0, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', line 382, code _exec_file>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', lineno=380, function='_exec_file', code_context=['                                                 raise_exceptions=True)\n'], index=0),
 FrameInfo(frame=<frame at 0x5613157b1a20, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', line 412, code _run_startup_files>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', lineno=409, function='_run_startup_files', code_context=['                self._exec_file(fname)\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48de45a810, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', line 333, code init_code>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/core/shellapp.py', lineno=318, function='init_code', code_context=['        self._run_startup_files()\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48dee48210, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/terminal/ipapp.py', line 323, code initialize>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/terminal/ipapp.py', lineno=323, function='initialize', code_context=['        self.init_code()\n'], index=0),
 FrameInfo(frame=<frame at 0x5613153d4370, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/traitlets/config/application.py', line 87, code catch_config_error>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/traitlets/config/application.py', lineno=87, function='catch_config_error', code_context=['        return method(app, *args, **kwargs)\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48dee1fad0, file '<decorator-gen-113>', line 2, code initialize>, filename='<decorator-gen-113>', lineno=2, function='initialize', code_context=None, index=None),
 FrameInfo(frame=<frame at 0x56131559a2f0, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/traitlets/config/application.py', line 664, code launch_instance>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/traitlets/config/application.py', lineno=663, function='launch_instance', code_context=['        app.initialize(argv)\n'], index=0),
 FrameInfo(frame=<frame at 0x5613156a15f0, file '/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/__init__.py', line 126, code start_ipython>, filename='/opt/conda_envs/collection-2021-1.0/lib/python3.7/site-packages/IPython/__init__.py', lineno=126, function='start_ipython', code_context=['    return launch_new_instance(argv=argv, **kwargs)\n'], index=0),
 FrameInfo(frame=<frame at 0x7f48e09cc450, file '/opt/conda_envs/collection-2021-1.0/bin/ipython', line 11, code <module>>, filename='/opt/conda_envs/collection-2021-1.0/bin/ipython', lineno=11, function='<module>', code_context=['    sys.exit(start_ipython())\n'], index=0)]
$ cat ~/.ipython/profile_collection/startup/00-base.py
print(f'Loading {__file__}...')

from ophyd import Device, Signal, Component as Cpt


class MyDev(Device):
    sig = Cpt(Signal)
$ cat ~/.ipython/profile_collection/startup/01-devices.py
print(f'Loading {__file__}...')

my_dev = MyDev('', name='my_dev')

@mrakitin
Copy link
Member Author

@klauer, @prjemian, @danielballan, @tacaswell, what are your thoughts about it?

I remember we discussed something along these lines with Ken long time ago, but I forgot where, and what was the result.

@klauer
Copy link
Member

klauer commented Jan 22, 2021

A few thoughts:

  • Do you really need to save the code frames? Would rather a few of the latest calling frames' source code filename plus line number suffice?
  • Does the PR really add that much overhead that it needs to be optional? I'm a bit surprised you said it was noticeably slower.
  • What does the IPython magic %edit obj do for the objects/classes of mysterious origin?

@danielballan
Copy link
Member

If we can solve this from the outside, like with %edit obj or ?obj, then we can use it on all kinds of objects with mysterious origin, like plan definitions, catalog instances, etc. I can’t think why this would be an ophyd-specific thing.

It’s possible that %edit and obj? are being kneecapped at NSLS by our use of scripts exec-ed a global namespace. As we move toward proper Python packages (and happi) they will be able to tell us more. That doesn’t help us in short term...but I would hate to permanently add weight to ophyd objects to work around this local deployment mistake.

@mrakitin
Copy link
Member Author

I created a demo repository to test this feature: https://github.com/mrakitin/profile_inspect_ophyd_pr961. The README.md file covers the questions raised by @klauer and @danielballan.

Do you really need to save the code frames? Would rather a few of the latest calling frames' source code filename plus line number suffice?

This is addressed via e7d08b6, good suggestion, Ken!


Does the PR really add that much overhead that it needs to be optional? I'm a bit surprised you said it was noticeably slower.

It does slow things down. You could check the timing report at the bottom of the https://github.com/mrakitin/profile_inspect_ophyd_pr961#with-ophyd_debug_with_inspect1 section. Posting it here for quicker access:

# Without OPHYD_DEBUG_WITH_INSPECT=1
In [1]: durations
Out[1]:
{'/home/vagrant/.ipython/profile_inspect_ophyd_pr961/startup/00-base.py': 0.565570912000112,
 '/home/vagrant/.ipython/profile_inspect_ophyd_pr961/startup/01-devices.py': 0.0007143089997043717}

# With OPHYD_DEBUG_WITH_INSPECT=1
In [1]: durations
Out[1]:
{'/home/vagrant/.ipython/profile_inspect_ophyd_pr961/startup/00-base.py': 42.23476164300064,
 '/home/vagrant/.ipython/profile_inspect_ophyd_pr961/startup/01-devices.py': 0.009821958999964409}

So, we have a ~40s overhead in this simple case, therefore I prefer to have this feature optional, and disabled by default.


What does the IPython magic %edit obj do for the objects/classes of mysterious origin?

Please check https://github.com/mrakitin/profile_inspect_ophyd_pr961#to-address-questions-about-edit-and-obj-ipython-magics. Unfortunately, the magic commands do not work for the objects defined in the profile startup files.

@mrakitin mrakitin marked this pull request as ready for review May 13, 2022 18:35
@klauer
Copy link
Member

klauer commented May 13, 2022

40+ seconds?! Wow...

I wonder if it's related to adding this information onto every ophyd areadetector class and such.
Could you limit the information to non-ophyd provided classes?
That is to say, not cls.__module__.startswith("ophyd.")?

@mrakitin
Copy link
Member Author

40+ seconds?! Wow...

I wonder if it's related to adding this information onto every ophyd areadetector class and such. Could you limit the information to non-ophyd provided classes? That is to say, not cls.__module__.startswith("ophyd.")?

Good suggestion! It indeed speeds it up quite a bit:

In [1]: durations
Out[1]:
{'/home/vagrant/.ipython/profile_inspect_ophyd_pr961/startup/00-base.py': 0.665280208000695,
 '/home/vagrant/.ipython/profile_inspect_ophyd_pr961/startup/01-devices.py': 0.006712106000122731}

Copy link
Member

@klauer klauer left a comment

Choose a reason for hiding this comment

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

Seems good to me overall. It could be useful 👍

@@ -10,6 +11,11 @@
from .log import control_layer_logger


OPHYD_DEBUG_WITH_INSPECT = os.getenv('OPHYD_DEBUG_WITH_INSPECT')
if OPHYD_DEBUG_WITH_INSPECT:
import inspect
Copy link
Member

Choose a reason for hiding this comment

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

Small standard-library import - I'd say include it in the normal import block above.

You might consider something like this to avoid confusion about how to enable it:
os.environ.get("...", "") in ("y", "yes", "1")

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 idea, I'll add a better check soon.

Copy link
Member

@ZLLentz ZLLentz left a comment

Choose a reason for hiding this comment

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

Looks useful to me 👍

@@ -10,6 +11,11 @@
from .log import control_layer_logger


OPHYD_DEBUG_WITH_INSPECT = os.getenv('OPHYD_DEBUG_WITH_INSPECT')
Copy link
Member

Choose a reason for hiding this comment

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

Do you imagine this being the only thing we'll want to observe more closely in a "slow" debug build?
If not, we'll probably end up renaming this later- the naming is very specific to this one case.

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 point, @ZLLentz! We may have other use-cases for inspect, so I think naming it as OPHYD_DISCOVER_CLASSES_SOURCE (or something similar) will reflect the purpose of the environment variable. I probably also need to add it to the documentation 🤔.

Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, going the other direction and making it OPHYD_DEBUG could make it easier to switch on/off general debugging tools. I think @ZLLentz may have been pointing that way.

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.

4 participants