Page MenuHomeWildfire Games

Remove CMapGeneratorWorker struct AutoFree
ClosedPublic

Authored by elexis on Jul 16 2019, 4:48 PM.

Details

Summary

rP15568 introduced a memory leak that (m_ScriptInterface not deleted in case of early return).
This was fixed in rP15569 by adding a struct Autofree that deletes the pointer in its dtor when the early return or function/scope end was reached.
That deleted too soon, before JSAutoRequest was deleted, which was fixed in rP15572.
rP20035 fixed the MapReader not being deleted in case of mapgen abort, which leaked its CMapGeneratorWorker which leaked its m_ScriptInterface, which triggered a debug breakpoint mandating all runtimes to be deleted prior to something else being deleted.

Test Plan

The CMapGeneratorWorker thread either succeeds and reaches the delete point, or it is abandoned and reaches the delete point.
The CMapGeneratorWorker dtor waits for the thread to finish one way or another.
The crash of rP15572 is not triggered because the JSAutoRequest is deleted when exiting the Run function prior to deleting m_ScriptInterface.
rP20035 ensures that the CMapGeneratorWorker dtor is always called.
What is the obvious I missed, or can the struct construction and its warnings be safely removed?

Diff Detail

Repository
rP 0 A.D. Public Repository
Lint
Automatic diff as part of commit; lint not applicable.
Unit
Automatic diff as part of commit; unit tests not applicable.

Event Timeline

elexis created this revision.Jul 16 2019, 4:48 PM
elexis edited the test plan for this revision. (Show Details)Jul 16 2019, 4:50 PM

Successful build - Chance fights ever on the side of the prudent.

Link to build: https://jenkins.wildfiregames.com/job/docker-differential/95/display/redirect

All I can think of are exceptions. What if an exception is thrown in CMapGeneratorWorker::Run()?

If a mapgen script throws an error, or if the user hits alt+f4 (SQL quit event), then LoadGlobalScriptFile (it's JS::Evaluate call`) and subsequently Run will return false.

Inserting a throw new Error("blerp"); to new_rms_test.js for instance:

Generating New RMS Test of size 256 and 4 players.
ERROR: JavaScript error: maps/random/new_rms_test.js line 6
Error: blerp
  @maps/random/new_rms_test.js:6:7
ERROR: CMapGeneratorWorker::Run: Failed to load RMS 'maps/random/new_rms_test.js'

and handing over a sigterm:

Creating sand patches...WARNING: Quit requested!
WARNING: JavaScript warning: maps/random/rmgen/Area.js line 11
Script terminated by timeout at:
Area@maps/random/rmgen/Area.js:11:15
createArea@maps/random/rmgen/library.js:198:13
createAreas/placeFunc@maps/random/rmgen/library.js:133:10
retryPlacing@maps/random/rmgen/library.js:102:16
createAreas@maps/random/rmgen/library.js:136:9
@maps/random/neareastern_badlands.js:115:1

WARNING: Quit requested!
WARNING: JavaScript warning: maps/random/rmgen/library.js line 136
Script terminated by timeout at:
createAreas@maps/random/rmgen/library.js:136:9
@maps/random/neareastern_badlands.js:115:1

the latter comes from rP21264.

WOuldn't it be better to make m_ScriptInterface an std::unique_ptr (and still delete)? That would be exception-safe, right?

For a C++ exception in Run() the following happens (with the currently committed code without this patch):

terminate called after throwing an instance of 'std::invalid_argument'
  what():  blurp

Thread 27 "pyrogenesis" received signal SIGABRT, Aborted.
[Switching to Thread 0x7fffa4bff700 (LWP 2118)]
0x00007ffff6137755 in raise () from /usr/lib/libc.so.6
(gdb) info stack
#0  0x00007ffff6137755 in raise () from /usr/lib/libc.so.6
#1  0x00007ffff6122851 in abort () from /usr/lib/libc.so.6
#2  0x00007ffff64d881f in __gnu_cxx::__verbose_terminate_handler () at /build/gcc/src/gcc/libstdc++-v3/libsupc++/vterminate.cc:95
#3  0x00007ffff64e530a in __cxxabiv1::__terminate (handler=<optimized out>) at /build/gcc/src/gcc/libstdc++-v3/libsupc++/eh_terminate.cc:47
#4  0x00007ffff64e5367 in std::terminate () at /build/gcc/src/gcc/libstdc++-v3/libsupc++/eh_terminate.cc:57
#5  0x00007ffff64e55bd in __cxxabiv1::__cxa_throw (obj=obj@entry=0x7fff84ef6b80, tinfo=0x7ffff661d458 <typeinfo for std::invalid_argument>, dest=0x7ffff64fb720 <std::invalid_argument::~invalid_argument()>)
    at /build/gcc/src/gcc/libstdc++-v3/libsupc++/eh_throw.cc:95
#6  0x00005555555ced13 in CMapGeneratorWorker::Run (this=0x7fffdc014750) at ../../../source/graphics/MapGenerator.cpp:175
#7  0x00005555559a4765 in CMapGeneratorWorker::RunThread (data=0x7fffdc014750) at ../../../source/graphics/MapGenerator.cpp:101
#8  0x00007ffff62c957f in start_thread () from /usr/lib/libpthread.so.0
#9  0x00007ffff61f90e3 in clone () from /usr/lib/libc.so.6

(09:01:24 PM) historicbruno: elexis, yeah something like CMapGeneratorWorker::LoadMapTerrain, it can throw a couple of exceptions

Equally triggering the throw PSERROR_File_InvalidVersion(); in this function (jebel barkal random map) triggers the same.

WOuldn't it be better to make m_ScriptInterface an std::unique_ptr (and still delete)? That would be exception-safe, right?

You mean making it a pointer in the tests and passing a pointer? I don't see the need to make it a pointer, unless there are reasons the AutoFree must be there.

All I can think of are exceptions. What if an exception is thrown in CMapGeneratorWorker::Run()?

As far as I see there should by design not be any exceptions thrown in the MapGenerator thread.
If there is an error in mapgen stage, it is passed as progress = -1 to the MapReader in CMapReader::GenerateMap.
One could do some effort to pass over the exception string.

I've created D2089 to change the two exceptions relevant in this file to JS Report Errors and catch the ones in MapReader unrelated from the code here.

Unless we want to throw exceptions in the MapGenerator class, this should be independent to this diff and the delete call would always be called (or the program terminated if there is a command throwing an uncaught exception).

historic_bruno added a comment.EditedJul 16 2019, 11:43 PM
In D2084#86963, @elexis wrote:

All I can think of are exceptions. What if an exception is thrown in CMapGeneratorWorker::Run()?

As far as I see there should by design not be any exceptions thrown in the MapGenerator thread.
If there is an error in mapgen stage, it is passed as progress = -1 to the MapReader in CMapReader::GenerateMap.
One could do some effort to pass over the exception string.

Sounds reasonable to me.

historic_bruno accepted this revision.Jul 16 2019, 11:49 PM
This revision is now accepted and ready to land.Jul 16 2019, 11:49 PM
vladislavbelov added inline comments.
source/graphics/MapGenerator.cpp
108 ↗(On Diff #8929)

It's not equal to SAFE_DELETE, you don't do self->m_ScriptInterface = nullptr. Is that expected?

elexis added inline comments.Jul 17 2019, 12:05 AM
source/graphics/MapGenerator.cpp
108 ↗(On Diff #8929)

I chose delete over SAFE_DELETE because it's an error if self->m_ScriptInterface is null but it doesn't complain about it. Implied assert.
I can change to the macro, but it seems like silently hiding an error and pretending nothing happened.
Not setting to null shouldn't be a problem since the thread is exited right after.
Or the thread runs forever if the mapscrpt never calls ExportMap and never reaches the delete.

source/graphics/MapGenerator.cpp
108 ↗(On Diff #8929)

We should check for a nullptr in a nicer way than that (not rely on mysterious crashes).

According to current coding conventions:

If deleting a pointer, and it's not in a destructor, and it's not being immediately assigned a new value, use "SAFE_DELETE(p)" (which is equivalent to "delete p; p = nullptr;") to avoid dangling pointers to deleted memory.

I guess it could be argued whether it's essentially in a destructor or not, but I think an assignment is cheap, safer, and might prevent future errors.

historic_bruno requested changes to this revision.Jul 17 2019, 12:15 AM
This revision now requires changes to proceed.Jul 17 2019, 12:15 AM
elexis updated this revision to Diff 8944.Jul 17 2019, 12:34 AM

Set to NULL pointer after delete.

source/graphics/MapGenerator.cpp
108 ↗(On Diff #8929)

We should check for a nullptr in a nicer way than that (not rely on mysterious crashes).

Actually it doesn't:

delete (p); /* if p == 0, delete is a no-op */ \

So SAFE_DELETE is superior indeed.

Successful build - Chance fights ever on the side of the prudent.

Link to build: https://jenkins.wildfiregames.com/job/docker-differential/108/display/redirect

source/graphics/MapGenerator.cpp
108 ↗(On Diff #8929)

Sorry, I meant your point that m_ScriptInterface might be null (fail to create somehow). Then if we access it, there will be a crash. Or did you not mean that?

historic_bruno accepted this revision.Jul 17 2019, 12:56 AM
This revision is now accepted and ready to land.Jul 17 2019, 12:56 AM
elexis added inline comments.Jul 17 2019, 12:57 AM
source/graphics/MapGenerator.cpp
108 ↗(On Diff #8929)

I thought delete null; crashes, but it doesn't.

elexis retitled this revision from Remove CMapGeneratorWorker struct AutoFree? to Remove CMapGeneratorWorker struct AutoFree.Jul 17 2019, 1:40 AM
This revision was automatically updated to reflect the committed changes.
Owners added a subscriber: Restricted Owners Package.Jul 17 2019, 2:07 AM
In D2084#86963, @elexis wrote:

WOuldn't it be better to make m_ScriptInterface an std::unique_ptr (and still delete)? That would be exception-safe, right?

You mean making it a pointer in the tests and passing a pointer? I don't see the need to make it a pointer, unless there are reasons the AutoFree must be there.

You mean those tests: D2085 ?
I don't understand your comment, m_ScriptInterface is already a pointer? If instead the script interface is only needed in Run(), it should be defined there as a non-pointer.

I don't understand your comment, m_ScriptInterface is already a pointer? If instead the script interface is only needed in Run(), it should be defined there as a non-pointer.

It's a pointer in MapGenerator but not test_MapGenerator, and the the Run() function can't be changed to consume the scriptInterface as an argument since the script functions will also access m_ScriptInterface, for example LoadScripts, as those will operate on a scriptinterface again. I suppose that could be pulled from the private context instead of the member, but it doesn't look more clear to me if the functions will use a scriptinterface that is different from the member one. (Unless you propose to drop MapGenerator.cpp completely somehow from the testcase)

In D2084#87060, @elexis wrote:

It's a pointer in MapGenerator but not test_MapGenerator, and the the Run() function can't be changed to consume the scriptInterface as an argument since the script functions will also access m_ScriptInterface, for example LoadScripts, as those will operate on a scriptinterface again.

Right. As said on D2085 I don't think you need to use the map worker's script interface to unit test map script stuff though.

(Unless you propose to drop MapGenerator.cpp completely somehow from the testcase)

Indeed that is what I do