-
Notifications
You must be signed in to change notification settings - Fork 186
Skip with statements automatically where needed #272
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
base: fixes
Are you sure you want to change the base?
Conversation
Nevermind, I hadn't run all the tests... |
I think that this is an interesting idea and we shouldn't be so prompt in rejecting it. Don't worry about failing tests, 2.0 is still in dev, and breaking stuff is expected going to this new major release. Moreover, as far as I can see, tests that broke are those introduced in the I am a little bit more concerned with performances, it seems to me that this adds a lot of work on top of simple function calls. However, this is already the case for the
Can you develop? I am not sure to understand all implications of your proposal, I'll have to dig deeper into it to grasp a better understanding. Anyway, thanks for the help :) |
I'm glad you also think this is an interesting idea. My main concern, that I only realized after submitting the PR, is that this simply cuts off all the content of the with block, meaning that if the user has some custom logic inside it, it will be completely ignored. Perhaps we could make this feature togglable? Some food for thought... Also about the tests, what put me off is that after realizing that above, then the failing tests that concerned me the most where mainly about catching exceptions inside the modified with statements, and since I already understand little about how the method to skip the with blocks works, I was intimidated to look into how exception traces work and then gave up a bit too quickly. However I'm willing to look more into it since you also think this might be a good idea, so let me know about the first concern with skipping custom user logic. PS: forgot to touch on performance. Sure this does impact performance, it has to instantiate a whole new class for everyone of those begin's since I couldn't manage to implement this directly in Cython, firstly because I am not experienced with it, and secondly because this uses some weird internal types for system traces that I'm not sure exist on the C side... If someone more acquainted with Cython is willing to look into this I'd be very grateful. However from my testing the overhead was not noticeable at all, but I didn't run any particular tests, I simply ran an example window and it reached the stable 60 fps without issues. I'll test properly when I have time. |
A wrapper class is superfluous, using functions is faster and simpler Should also fix AttributeErrors accessing .opened and analogous
I was thinking with my OOP brain and not with a logical train of thought... a wrapper class is completely overkill for this purpose, simple functions do the job just fine. This way the logic for the |
Performance wise I get fps between high 550s and low 560s fps with if-style code, while in the mid 550s using the with-style code. Looks to me like this Also about the 3 failing tests it's obvious: these tests check if an exception can be thrown from inside the So the last concern that remains in my opinion is the same I had originally:
I'm open to any kind of suggestion! |
Thanks for the clarification and proposal!
Ok, I understand it now.
Indeed, this looks cleaner and seems to fix the previously raised issue. However I have some doubts about the Concerning perfs, I am not sure that looking at FPS count is a great way to measure it. This is influenced by too many other things and I think that perf problems will show only for big projects which extensively use the functionality, i.e., with many calls by frame. In my opinion, a better measurement technic is to isolate as much as possible some examples of use in small functions (similarly to test cases) and use
(I also quickly tested other types of begin/end) This isn't by any means an extensive measurement and may need more rigor, but we can already note two things:
This may be accelerated using cython instead of python, I'll need to dig deeper into that. For now, I am not sure if this behavior should be the default one. It is clearly more programmer-friendly but the penalty feels a bit harsh for such a used feature of the lib. Note that this also raises concerns with regard to the original Yes, it could be a togglable feature but wouldn't it also cost a set of conditional around each use of the feature? And wouldn't it be a bit confusing at use? I still think the whole thing is a good idea and I would prefer to have it as default or not have it at all, but we need to figure out if we can make it as efficient as possible. This pattern is used everywhere in imgui and if we don't take special care with it it will clumps up and bite us in the back. Note: We not only need to consider the raw timing of function calls but also how many times we expect those calls to occur in a "normal" application in order to estimate the total penalty. Note: I would be curious to compare perf on an old branch without the |
Oh wow. Yea that is very slow, indeed FPS is a horrible way to test this. I'll try to see what can be done through Cython. About the global and nested skipped-with, I too had that concern initially, that's partly why I originally went with a class wrapper, so it would be assigned to the class and not globally. However, if you think about it, when entering a I, too, think that this should either be default or not exist at all, and yes togglable would mean additional if checks and more complexity on user side. So yeah I'll see what can be done in terms of optimization, and if nothing can be done then we'll just have to let this go, no big deal. |
Mmmh yeah, you make a point here.
To be fair, it is not that slow. For a 60fps target, we have a 16000µs budget per frame. I do not have an official number for the average number of call but it doesn't seem to be reasonable to expect ~1000 usage of the functionality per frame (note that the frame budget would probably be spent elsewhere in a typical application which lowers considerably this bound). Moreover, since the other usages are conserved and since they do not imply a noticeable overhead, if someone needs to get every cycle he can he always has the possibility to use the If we can get some little more by going to cython it would be great, else I am still willing to include the functionality even in this state. |
Ok so, bit of an update. I am yet try try porting to Cython, but I tried simplifying the code itself a bit: The calls this makes to the frame = inspect.currentframe().f_back
frame.f_trace = raise_skip_with_statement can be replaced with: sys._getframe(1).f_trace = raise_skip_with_statement
From what I understand, Edit: not having the code you used for your tests I quickly made my own snippet, and applying the changes I outlined above my timings drop from import timeit
import imgui
import pint
u = pint.UnitRegistry()
imgui.create_context()
io = imgui.get_io()
io.delta_time = 1.0 / 60.0
io.display_size = 300, 300
io.fonts.get_tex_data_as_rgba32()
io.fonts.add_font_default()
io.fonts.texture_id = 0
imgui.new_frame()
def popup():
with imgui.begin_popup_modal(""):
print("DID NOT SKIP") # Never got printed
took = timeit.timeit(popup, number=1000000) * u.seconds
avg = took / 1000000
print(avg.to(u.microseconds))
imgui.render()
imgui.end_frame() |
And update 2: can't manage to get it working in cython. It compiles fine, but even just copy pasting the same exact python code from DID NOT SKIP
Traceback (most recent call last):
File "/home/willyjl/Coding/test/test3.py", line 21, in <module>
popup()
File "/home/willyjl/Coding/test/test3.py", line 16, in popup
with imgui.begin_popup_modal(""):
File "imgui/core.pyx", line 5412, in imgui.core._BeginEndPopupModal.__exit__
if self.opened:
File "imgui/core.pyx", line 5412, in __exit__
if self.opened:
TypeError: 'NoneType' object is not callable Also tried making a new But do let me know your test results with the optimizations i made above, it might be enough to make a difference. Edit: the issue appears to be setting the tracing function, as per cython/cython#1769 |
Ok, I've spent way too long on this and I can't get any further than this: The issue with For some reason All the trace function stuff is doable in Cython without relying on the Had to change Results are:
Not sure why my ratios are so different from the ones you got in your tests... anyway that's the best I can get it to run. Also sorry for blabbering on forever, I just like documenting and explaining what I do as a way of record keeping :D |
Great job, thanks!
I did my test again using the same commits as you and I get the following results:
Scale is different than yours due to different hardware and we don't measure exactly the same function. However, the ratios seem similar. Strangely, 'with+if' seems to do way worse than in my first experiments, I probably did something wrong back there, sorry. Anyway, this is a huge improvement with respect to the original proposal, and a significant one with respect to your second attempt. In my opinion (but should be measured to be sure), the bottleneck is probably the python call to Looking at sys documentation I saw this information on both
This makes me a little bit worried. We may have to go to the full custom implementation in cython just to be sure to consistently have the functionality on every machine. What is your opinion about that? I'll try to study this option and see what I can do.
I think this is because you are using |
I have a few thoughts and a possible alternative which would solve this issue in a more pythonic manner.
I share the same concerns about using Here is a proof of concept of an alternative solution that I have devised. from contextlib import contextmanager
class _ImGUIOpenError(Exception):
pass
@contextmanager
def begin(dummy):
try:
yield dummy
except _ImGUIOpenError:
print("The inside of the with statement was skipped as opened == False")
finally:
# close this
pass
@contextmanager
def require_opened(value):
if not value.get("opened"):
raise _ImGUIOpenError()
yield value
pass
with begin({"opened": True}) as ctx, require_opened(ctx):
print("This will execute as expected because opened is True")
with begin({"opened": False}) as ctx, require_opened(ctx):
print("This will never execute as because opened == False", ctx)
with begin({"opened": True}) as ctx:
if ctx.get("opened", False):
print("You can also use this context manager in this way")
with begin({"opened": True}) as ctx, require_opened(ctx):
# Anything here will execute as expected
raise Exception("This exception will be thrown as normal and exit the program") The output of this above code would be as expected:
Note that the use of @contextlib.contextmanager was just a choice to make my example more concise. The documentation covers this helper decorator quite well. This can be replicated using traditional class based context managers (this may even be more performant). I think such a solution would be preferred as there is no user code being skipped without being explicitly skipped as intended. There probably is a way to couple those two context managers together and have something like: with imgui.strict_begin_popup_modal(""):
# This would only happen popup.opened == True All the names are just placeholders, there is probably a better name for such a function |
Agreed, this looks like the best compromise. Thanks, I'll look into implementing this properly tomorrow :D |
Can't manage to set that up, the exception always propagates... However the main proposal could be even simpler, I was thinking something like: # core.pyx
class _ImGUIOpenError(Exception):
pass
class require(object):
def __init__(self, value):
self.value = value
def __enter__(self):
if not self.value:
raise _ImGUIOpenError()
def __exit__(self, exc_type, exc_val, exc_tb):
pass
class _BeginEndPopupModal(object):
...
def __exit__(self, exc_type, exc_val, exc_tb):
...
if exc_type is _ImGUIOpenError:
return True
# userland
with imgui.begin_popup_modal("Modal Popup") as popup, imgui.require(popup.opened):
imgui.text("I'm a popup!") This would mean that this whole PR could boil down to just a single generic cython class that skips the with block on a simple bool condition. Or we could have a skip function on the context itself, which might be more intuitive, and not to mention faster in perf terms: # core.pyx
class _ImGUIOpenError(Exception):
pass
class _BeginEndPopupModal(object):
...
def __exit__(self, exc_type, exc_val, exc_tb):
...
if exc_type is _ImGUIOpenError:
return True
...
def skip_with_if_needed(self):
if not self.opened:
raise _ImGUIOpenError()
# userland
with imgui.begin_popup_modal("Modal Popup") as popup, popup.skip_with_if_needed():
imgui.text("I'm a popup!") (Of course both of those are still pure python, when we decide I'll convert to cython code.) Personally I'm fine with both but I'd prefer the latter, but do let me know what you think! Edit: I haven't done any extensive testing yet, but while on 8bbe86b (the last commit I pushed here, with cythonized withskip) I get |
Hey @Willy-JL, @KinoxKlark, @apadhi20, and @mCodingLLC. I know I wasn't active here for long time but this is something I've been thinking about for a very long time. I gave a shot to it after seeing James' video today without knowing any latest changes in dev branch and or even this PR. By the way, good job on #255 and video explainer @mCodingLLC! Long story short, I came to conclusion that the easiest way would be to have extra function in imgui core and/or method in the In my opinion the most expressive way for those context would be as in following example: with imgui.begin_popup_modal("Modal Popup") as popup, imgui.only_if(popup.opened):
... Another approach could be having a breakout method method on those context tuples (let's call them so :P): with imgui.begin_popup_modal("Modal Popup") as popup:
imgui.breakout_if(not popup.opened) I'm not really sure about the second option and its naming (maybe break?, maybe without argument?) but I'm pretty sure you can all figure it out. Also, I can envision situation when this could be more readable to avoid long |
Thanks for the input, and glad to see you're still interested in pyimgui @swistakm! Actually that's exactly what was missing in my opinion, some proper naming! I personally feel like the "only if" paradigm fits perfectly in this situation. However having a module wide function for that I feel like could be counter intuitive, and could lead curious users to using it wrongly and be left wondering what on earth a with imgui.begin_popup_modal("Modal Popup") as popup, popup.only_if_open():
... With this option the "skipping with blocks" is entirely bound to their relative begin/open functions and context tuples, so in my opinion it feels more natural and makes it clear that they should only be used in this particular use case.
Well how this works is that the with imgui.begin_popup_modal("Modal Popup") as popup:
popup.only_if_open()
... would work exactly in the same way, out of the box, with no additional code required. Personally I feel like this is the most natural solution to implementing this feature, but do let me know what you all think about this. EDIT: I have updated the OP for this issue with the new status quo and implemented it the way I described above. I am yet to test the timings I get with the new changes |
…into skip-with-statements
Just ran some tests on 6ae5768:
Just so we're on the same page, this is how I got these results: import timeit
import imgui
import pint
u = pint.UnitRegistry()
imgui.create_context()
io = imgui.get_io()
io.delta_time = 1.0 / 60.0
io.display_size = 300, 300
io.fonts.get_tex_data_as_rgba32()
io.fonts.add_font_default()
io.fonts.texture_id = 0
def if_beginend_popup():
if imgui.begin_popup("Example"):
imgui.text("Example")
imgui.end_popup()
def with_if_opened():
with imgui.begin_popup("Example") as popup:
if popup.opened:
imgui.text("Example")
def with_only_if():
with imgui.begin_popup("Example") as popup, popup.only_if_open():
imgui.text("Example")
def with_only_if_newline():
with imgui.begin_popup("Example") as popup:
popup.only_if_open()
imgui.text("Example")
imgui.new_frame()
average = lambda func: (timeit.timeit(func, number=100000)*u.seconds / 100000).to(u.microseconds).magnitude
print(f"if begin/end: {average(if_beginend_popup):.3f} µs")
print(f"with/if opened: {average(with_if_opened):.3f} µs")
print(f"with/only if: {average(with_only_if):.3f} µs")
print(f"with/only if newline: {average(with_only_if_newline):.3f} µs")
imgui.render()
imgui.end_frame() Can't say that I'm over the moon with these results, but also this might be the best we can get without making it the default behavior... and in the grand scheme of things it's not the end of the world for a third of a microsecond... Let me know your thoughts |
While the
with
context managers are very useful, it is annoying to have to remember theif xyz.opened
and analogous, and also the double indentation can get in the way. The solution outlined towards the end of #255 (having the user raise the exception and imgui catch it) would solve this but it is not that ideal since it still requires the user to remember to raise the exception, and also it is worse in terms of readability.I discovered a project that allowed skippingwith
blocks using dark magic (I'm not 100% sure on what exactly it does, but my best understanding is that it modifies the system trace to make python think the exception was raised just inside thewith
block, and then discards the exception in__exit__
) and tried to adapt it to this library.This PR implementsimgui._skipwith.SkipWith(context, should_skip)
, an internal class used to wrap an existing imgui context manager (first argument) and optionally skip thewith
block body based on a condition (second argument).Edit after d446049: This PR implements theskip_with_init()
andskip_with_cleanup()
internal functions, insideimgui._skipwith
, which allow to skip the body of awith
statement, and applies them to the relevant existing context managers, avoiding annoyingif xyz.opened
checks:Continues to work:Also continues to work (behavior of #255):Can be rewritten as:Edit after 6ae5768:
Skipping with blocks by default could be very counter intuitive and cause unneeded confusion for the users. Instead, the proposed solution is an additional
only_if_x()
method on the classes returned by thebegin_x()
, which would allow the users to request skipping the with block's content, without it being the default behavior.Continues to work:
Also continues to work (behavior of #255):
Can be rewritten as:
Or also, to avoid long "with" lines: