[lldb-dev] Python object lifetimes affect the reliability of tests

Ryan Brown via lldb-dev lldb-dev at lists.llvm.org
Thu Oct 15 11:36:26 PDT 2015


Couldn't we just change DeleteTarget to make sure everything is unmapped?

On Thu, Oct 15, 2015 at 11:34 AM Zachary Turner via lldb-dev <
lldb-dev at lists.llvm.org> wrote:

> To add more evidence for this, here's a small repro:
>
> import sys
>
> print "sys.exc_info() = ", "Empty" if sys.exc_info() == (None, None, None)
> else "Valid"
> try:
>     raise Exception
> except Exception, e:
>     print "sys.exc_info() = ", "Empty" if sys.exc_info() == (None, None,
> None) else "Valid"
>     pass
>
> print "sys.exc_info() = ", "Empty" if sys.exc_info() == (None, None, None)
> else "Valid"
> print "e = ", "Bound" if 'e' in vars() else "Unbound"
> pass
>
> For me this prints
> sys.exc_info() =  Empty
> sys.exc_info() =  Valid
> sys.exc_info() =  Valid
> e =  Bound
>
> On Thu, Oct 15, 2015 at 11:21 AM Zachary Turner <zturner at google.com>
> wrote:
>
>> We actually do already to the self.dbg.DeleteTarget(target), and that's
>> the line that's failing.  The reason it's failing is because the 'sc'
>> reference is still alive, which is holding an mmap, which causes a
>> mandatory file lock on Windows.
>>
>> The diagnostics went pretty deep into python internals, but I think we
>> might have figured it out.  I don't know if this is a bug in Python, but I
>> think we'd probably need to ask Guido to be sure :)
>>
>> As far as we can tell, what happens is that on the exceptional codepath
>> (e.g the assert fails), you walk back up the stack until you get to the
>> except handler.  This exception handler is in TestCase.run().  After it
>> handles the exception it goes and runs teardown.  However, for some reason,
>> Python is still holding a strong reference to the *traceback*, even though
>> we're completely out of the finally block.  What this means is that if you
>> call `sys.exc_info()` *even after you've exited the finally block, it still
>> returns info about the previous exception that's not even being handled
>> anymore.  I would have expected this to be gone since there's no exception
>> in-fligth anymore.  So basically, Python is still holding a reference to
>> the active exception, the exception holds the stack frame, the stack frame
>> holds the test method, the test method has locals, one of which is a
>> SymbolList, a member of which is symbol context, which has the file locked.
>>
>> Our best guess is that if you have something like this:
>>
>> def foo():
>>     try:
>>        # Do stuff
>>     except Exception, e:
>>        pass
>>     # Do more stuff
>>
>> that if the exceptional path is executed, then both e and sys.exc_info()
>> are alive *while* do more stuff is happening.  We've found two ways to
>> fixthis:
>>
>> 1) Change to this:
>> def foo():
>>     try:
>>        # Do stuff
>>     except Exception, e:
>>        pass
>>     del e
>>     sys.exc_clear()
>>     # Do more stuff
>>
>> 2) Put the try / except inside a function.  When the function returns,
>> sys.exc_info() is cleared.
>>
>> I like 2 better, but we're still testing some more to make sure this
>> really fixes it 100% of the time.
>>
>> On Thu, Oct 15, 2015 at 10:25 AM Greg Clayton via lldb-dev <
>> lldb-dev at lists.llvm.org> wrote:
>>
>>>
>>> > On Oct 15, 2015, at 8:50 AM, Adrian McCarthy via lldb-dev <
>>> lldb-dev at lists.llvm.org> wrote:
>>> >
>>> > I've tracked down a source of flakiness in tests on Windows to Python
>>> object lifetimes and the SB interface, and I'm wondering how best to handle
>>> it.
>>> >
>>> > Consider this portion of a test from TestTargetAPI:
>>> >
>>> >  def find_functions(self, exe_name):
>>> >      """Exercise SBTaget.FindFunctions() API."""
>>> >      exe = os.path.join(os.getcwd(), exe_name)
>>> >
>>> >      # Create a target by the debugger.
>>> >      target = self.dbg.CreateTarget(exe)
>>> >      self.assertTrue(target, VALID_TARGET)
>>> >      list = target.FindFunctions('c', lldb.eFunctionNameTypeAuto)
>>> >      self.assertTrue(list.GetSize() == 1)
>>> >
>>> >      for sc in list:
>>> >          self.assertTrue(sc.GetModule().GetFileSpec().GetFilename() ==
>>> exe_name)
>>> >          self.assertTrue(sc.GetSymbol().GetName() == 'c')
>>> >
>>> > The local variables go out of scope when the function exits, but the
>>> SB (C++) objects they represent aren't (always) immediately destroyed.  At
>>> least some of these objects keep references to the executable module in the
>>> shared module list, so when the test framework cleans up and calls
>>> `SBDebugger::DeleteTarget`, the module isn't orphaned, so LLDB maintains an
>>> open handle to the executable.
>>>
>>> Creating a target with:
>>>
>>>         target = self.dbg.CreateTarget(exe)
>>>
>>> Will give you a SBTarget object that has a strong reference to the
>>> target, but the debugger still has a copy in its target list, so the
>>> SBTarget isn't designed to delete the object when the target variable goes
>>> out of scope. If you want the target to be deleted, you actually have to
>>> call through to the debugger with:
>>>
>>>
>>>  bool
>>>  SBDebugger:DeleteTarget (lldb::SBTarget &target);
>>>
>>>
>>> So the right way to clean up the target is:
>>>
>>>  self.dbg.DeleteTarget(target);
>>>
>>> Even though there might be code within LLDB that has a valid shared
>>> pointer to the lldb_private::Target still, it calls
>>> lldb_private::Target::Destroy() which clears out most instance variable
>>> (the module list, the process, any plug-ins, etc).
>>>
>>> SBTarget objects have strong references so that they _can_ keep the
>>> object alive if needed in case someone else destroys the target on another
>>> thread, but they don't control the lifetime of the target.
>>>
>>> Other objects have weak references to the objects: SBProcess, SBThread,
>>> SBFrame. If the objects are actually destroyed already, the weak pointer
>>> won't be able to get a valid shared pointer to the underlying object
>>> and any SB API calls on these objects will return error, none, zero,
>>> etc...
>>>
>>> >
>>> > The result of the lingering handle is that, when the next test case in
>>> the test suite tries to re-build the executable, it fails because the file
>>> is not writable.  (This is problematic on Windows because the file system
>>> works differently in this regard than Unix derivatives.)  Every subsequent
>>> case in the test suite fails.
>>> >
>>> > I managed to make the test work reliably by rewriting it like this:
>>> >
>>> >  def find_functions(self, exe_name):
>>> >      """Exercise SBTaget.FindFunctions() API."""
>>> >      exe = os.path.join(os.getcwd(), exe_name)
>>> >
>>> >      # Create a target by the debugger.
>>> >      target = self.dbg.CreateTarget(exe)
>>> >      self.assertTrue(target, VALID_TARGET)
>>> >
>>> >      try:
>>> >          list = target.FindFunctions('c', lldb.eFunctionNameTypeAuto)
>>> >          self.assertTrue(list.GetSize() == 1)
>>> >
>>> >          for sc in list:
>>> >              try:
>>> >
>>> self.assertTrue(sc.GetModule().GetFileSpec().GetFilename() == exe_name)
>>> >                  self.assertTrue(sc.GetSymbol().GetName() == 'c')
>>> >              finally:
>>> >                  del sc
>>> >
>>> >      finally:
>>> >          del list
>>> >
>>> > The finally blocks ensure that the corresponding C++ objects are
>>> destroyed, even if the function exits as a result of a Python exception
>>> (e.g., if one of the assertion expressions is false and the code throws an
>>> exception).  Since the objects are destroyed, the reference counts are back
>>> to where they should be, and the orphaned module is closed when the target
>>> is deleted.
>>> >
>>> > But this is ugly and maintaining it would be error prone.  Is there a
>>> better way to address this?
>>>
>>> So you should be able to fix this by deleting the target with
>>> "self.dbg.DeleteTarget(target)"
>>>
>>> We could change all tests over to always store any targets they create
>>> in the test object itself:
>>>
>>> self.target = self.dbg.CreateTarget(exe)
>>>
>>> Then the test suite could check for the existance of "self.target" and
>>> if it exists, it could call "self.dbg.DeleteTarget(self.target)"
>>> automatically to avoid such issues?
>>>
>>>
>>>
>>> _______________________________________________
>>> lldb-dev mailing list
>>> lldb-dev at lists.llvm.org
>>> http://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-dev
>>>
>> _______________________________________________
> lldb-dev mailing list
> lldb-dev at lists.llvm.org
> http://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-dev
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/lldb-dev/attachments/20151015/f5de33aa/attachment.html>


More information about the lldb-dev mailing list