Skip to content

Commit c02cb8c

Browse files
GDScript: Respect external refs when cleaning up cyclic refs
1 parent f0aeea2 commit c02cb8c

File tree

2 files changed

+164
-117
lines changed

2 files changed

+164
-117
lines changed

modules/gdscript/gdscript.cpp

Lines changed: 159 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,69 +1261,6 @@ GDScript *GDScript::get_root_script() {
12611261
return result;
12621262
}
12631263

1264-
RBSet<GDScript *> GDScript::get_dependencies() {
1265-
RBSet<GDScript *> dependencies;
1266-
1267-
_collect_dependencies(dependencies, this);
1268-
dependencies.erase(this);
1269-
1270-
return dependencies;
1271-
}
1272-
1273-
HashMap<GDScript *, RBSet<GDScript *>> GDScript::get_all_dependencies() {
1274-
HashMap<GDScript *, RBSet<GDScript *>> all_dependencies;
1275-
1276-
List<GDScript *> scripts;
1277-
{
1278-
MutexLock lock(GDScriptLanguage::singleton->mutex);
1279-
1280-
SelfList<GDScript> *elem = GDScriptLanguage::singleton->script_list.first();
1281-
while (elem) {
1282-
scripts.push_back(elem->self());
1283-
elem = elem->next();
1284-
}
1285-
}
1286-
1287-
for (GDScript *scr : scripts) {
1288-
if (scr == nullptr || scr->destructing) {
1289-
continue;
1290-
}
1291-
all_dependencies.insert(scr, scr->get_dependencies());
1292-
}
1293-
1294-
return all_dependencies;
1295-
}
1296-
1297-
RBSet<GDScript *> GDScript::get_must_clear_dependencies() {
1298-
RBSet<GDScript *> dependencies = get_dependencies();
1299-
RBSet<GDScript *> must_clear_dependencies;
1300-
HashMap<GDScript *, RBSet<GDScript *>> all_dependencies = get_all_dependencies();
1301-
1302-
RBSet<GDScript *> cant_clear;
1303-
for (KeyValue<GDScript *, RBSet<GDScript *>> &E : all_dependencies) {
1304-
if (dependencies.has(E.key)) {
1305-
continue;
1306-
}
1307-
for (GDScript *F : E.value) {
1308-
if (dependencies.has(F)) {
1309-
cant_clear.insert(F);
1310-
}
1311-
}
1312-
}
1313-
1314-
for (GDScript *E : dependencies) {
1315-
if (cant_clear.has(E) || ScriptServer::is_global_class(E->get_fully_qualified_name())) {
1316-
continue;
1317-
}
1318-
must_clear_dependencies.insert(E);
1319-
}
1320-
1321-
cant_clear.clear();
1322-
dependencies.clear();
1323-
all_dependencies.clear();
1324-
return must_clear_dependencies;
1325-
}
1326-
13271264
bool GDScript::has_script_signal(const StringName &p_signal) const {
13281265
if (_signals.has(p_signal)) {
13291266
return true;
@@ -1370,55 +1307,60 @@ GDScript *GDScript::_get_gdscript_from_variant(const Variant &p_variant) {
13701307
return Object::cast_to<GDScript>(obj);
13711308
}
13721309

1373-
void GDScript::_collect_function_dependencies(GDScriptFunction *p_func, RBSet<GDScript *> &p_dependencies, const GDScript *p_except) {
1310+
void GDScript::_collect_function_dependencies(GDScriptFunction *p_func, RBMap<GDScript *, int> &r_dependencies) {
13741311
if (p_func == nullptr) {
13751312
return;
13761313
}
13771314
for (GDScriptFunction *lambda : p_func->lambdas) {
1378-
_collect_function_dependencies(lambda, p_dependencies, p_except);
1315+
_collect_function_dependencies(lambda, r_dependencies);
13791316
}
13801317
for (const Variant &V : p_func->constants) {
13811318
GDScript *scr = _get_gdscript_from_variant(V);
1382-
if (scr != nullptr && scr != p_except) {
1383-
scr->_collect_dependencies(p_dependencies, p_except);
1319+
if (scr != nullptr) {
1320+
r_dependencies[scr] += 1;
13841321
}
13851322
}
13861323
}
13871324

1388-
void GDScript::_collect_dependencies(RBSet<GDScript *> &p_dependencies, const GDScript *p_except) {
1389-
if (p_dependencies.has(this)) {
1390-
return;
1391-
}
1392-
if (this != p_except) {
1393-
p_dependencies.insert(this);
1394-
}
1395-
1325+
void GDScript::count_dependencies(RBMap<GDScript *, int> &r_res) {
13961326
for (const KeyValue<StringName, GDScriptFunction *> &E : member_functions) {
1397-
_collect_function_dependencies(E.value, p_dependencies, p_except);
1327+
_collect_function_dependencies(E.value, r_res);
13981328
}
13991329

14001330
if (implicit_initializer) {
1401-
_collect_function_dependencies(implicit_initializer, p_dependencies, p_except);
1331+
_collect_function_dependencies(implicit_initializer, r_res);
14021332
}
14031333

14041334
if (implicit_ready) {
1405-
_collect_function_dependencies(implicit_ready, p_dependencies, p_except);
1335+
_collect_function_dependencies(implicit_ready, r_res);
14061336
}
14071337

14081338
if (static_initializer) {
1409-
_collect_function_dependencies(static_initializer, p_dependencies, p_except);
1339+
_collect_function_dependencies(static_initializer, r_res);
14101340
}
14111341

1412-
for (KeyValue<StringName, Ref<GDScript>> &E : subclasses) {
1413-
if (E.value != p_except) {
1414-
E.value->_collect_dependencies(p_dependencies, p_except);
1342+
for (KeyValue<StringName, MemberInfo> &E : member_indices) {
1343+
Ref<GDScript> ref = E.value.data_type.script_type_ref;
1344+
if (ref.is_valid()) {
1345+
r_res[*ref] += 1;
14151346
}
14161347
}
14171348

1349+
for (KeyValue<StringName, MemberInfo> &E : static_variables_indices) {
1350+
Ref<GDScript> ref = E.value.data_type.script_type_ref;
1351+
if (ref.is_valid()) {
1352+
r_res[*ref] += 1;
1353+
}
1354+
}
1355+
1356+
for (KeyValue<StringName, Ref<GDScript>> &E : subclasses) {
1357+
r_res[*E.value] += 1;
1358+
}
1359+
14181360
for (const KeyValue<StringName, Variant> &E : constants) {
14191361
GDScript *scr = _get_gdscript_from_variant(E.value);
1420-
if (scr != nullptr && scr != p_except) {
1421-
scr->_collect_dependencies(p_dependencies, p_except);
1362+
if (scr != nullptr) {
1363+
r_res[scr] += 1;
14221364
}
14231365
}
14241366
}
@@ -1434,7 +1376,7 @@ GDScript::GDScript() :
14341376
path = vformat("gdscript://%d.gd", get_instance_id());
14351377
}
14361378

1437-
void GDScript::_save_orphaned_subclasses(ClearData *p_clear_data) {
1379+
void GDScript::_save_orphaned_subclasses() {
14381380
struct ClassRefWithName {
14391381
ObjectID id;
14401382
String fully_qualified_name;
@@ -1450,17 +1392,8 @@ void GDScript::_save_orphaned_subclasses(ClearData *p_clear_data) {
14501392
}
14511393

14521394
// clear subclasses to allow unused subclasses to be deleted
1453-
for (KeyValue<StringName, Ref<GDScript>> &E : subclasses) {
1454-
p_clear_data->scripts.insert(E.value);
1455-
}
14561395
subclasses.clear();
14571396
// subclasses are also held by constants, clear those as well
1458-
for (KeyValue<StringName, Variant> &E : constants) {
1459-
GDScript *gdscr = _get_gdscript_from_variant(E.value);
1460-
if (gdscr != nullptr) {
1461-
p_clear_data->scripts.insert(gdscr);
1462-
}
1463-
}
14641397
constants.clear();
14651398

14661399
// keep orphan subclass only for subclasses that are still in use
@@ -1546,6 +1479,83 @@ void GDScript::_recurse_replace_function_ptrs(const HashMap<GDScriptFunction *,
15461479
}
15471480
}
15481481

1482+
/**
1483+
* Implementation of Tarjan's Algorithm to find strongly connected dependency clusters (e.g. cycles): https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
1484+
*
1485+
* The implementation is not generalized and omits part of the algorithm, e.g. safeguards for disconnected graphs.
1486+
*/
1487+
class DependencyCyclesTarjanScc {
1488+
struct Data {
1489+
int index;
1490+
int lowlink;
1491+
int scc;
1492+
};
1493+
1494+
RBMap<GDScript *, Data> script_data;
1495+
LocalVector<GDScript *> stack;
1496+
int next_index = 0;
1497+
int next_scc_id = 0;
1498+
1499+
void _strong_connect(GDScript *p_script) {
1500+
if (!script_data.has(p_script)) {
1501+
script_data.insert(p_script, Data());
1502+
}
1503+
1504+
{
1505+
Data &data = script_data[p_script];
1506+
data.index = next_index;
1507+
data.lowlink = next_index;
1508+
data.scc = -1;
1509+
}
1510+
1511+
next_index += 1;
1512+
stack.push_back(p_script);
1513+
1514+
RBMap<GDScript *, int> dependencies;
1515+
p_script->count_dependencies(dependencies);
1516+
for (KeyValue<GDScript *, int> &dependency : dependencies) {
1517+
if (dependency.key == p_script) {
1518+
continue;
1519+
}
1520+
1521+
if (!script_data.has(dependency.key)) {
1522+
_strong_connect(dependency.key);
1523+
Data &data = script_data[p_script];
1524+
data.lowlink = MIN(data.lowlink, script_data[dependency.key].lowlink);
1525+
} else { // Omission from the original algorithm: Check whether `dependency.key` is on the stack. -> This condition can never be `false` when only starting from one node.
1526+
Data &data = script_data[p_script];
1527+
data.lowlink = MIN(data.lowlink, script_data[dependency.key].lowlink);
1528+
}
1529+
}
1530+
1531+
Data &data = script_data[p_script];
1532+
if (data.lowlink == data.index) {
1533+
RBSet<GDScript *> scc;
1534+
GDScript *head;
1535+
do {
1536+
head = stack[stack.size() - 1];
1537+
stack.resize(stack.size() - 1);
1538+
scc.insert(head);
1539+
script_data[head].scc = next_scc_id;
1540+
} while (head != p_script);
1541+
sccs.push_back(scc);
1542+
next_scc_id += 1;
1543+
}
1544+
}
1545+
1546+
public:
1547+
LocalVector<RBSet<GDScript *>> sccs;
1548+
1549+
DependencyCyclesTarjanScc(GDScript *p_from) {
1550+
// Omission from the original algorithm: Repeat for all not yet visited nodes. -> We only care about the subgraph of transitive dependencies.
1551+
_strong_connect(p_from);
1552+
}
1553+
1554+
int get_scc(GDScript *p_script) {
1555+
return script_data[p_script].scc;
1556+
}
1557+
};
1558+
15491559
void GDScript::clear(ClearData *p_clear_data) {
15501560
if (clearing) {
15511561
return;
@@ -1557,7 +1567,7 @@ void GDScript::clear(ClearData *p_clear_data) {
15571567
bool is_root = false;
15581568

15591569
// If `clear_data` is `nullptr`, it means that it's the root.
1560-
// The root is in charge to clear functions and scripts of itself and its dependencies
1570+
// The root is in charge to clear functions and scripts of itself and its orphaned dependencies.
15611571
if (clear_data == nullptr) {
15621572
clear_data = &data;
15631573
is_root = true;
@@ -1571,12 +1581,59 @@ void GDScript::clear(ClearData *p_clear_data) {
15711581
}
15721582

15731583
// If we're in the process of shutting things down then every single script will be cleared
1574-
// anyway, so we can safely skip this very costly operation.
1575-
if (!GDScriptLanguage::singleton->finishing) {
1576-
RBSet<GDScript *> must_clear_dependencies = get_must_clear_dependencies();
1577-
for (GDScript *E : must_clear_dependencies) {
1578-
clear_data->scripts.insert(E);
1579-
E->clear(clear_data);
1584+
// anyway, so we can safely skip this very costly operation. And if another root is clearing
1585+
// us up, it will take care of our deps as well.
1586+
if (is_root && !GDScriptLanguage::singleton->finishing) {
1587+
// The transitive dependencies of this script form a graph.
1588+
// 1. Use Tarjan DFS to find the graph and strongly connected components (SCC) in it. This also topologically sorts the SCCs.
1589+
DependencyCyclesTarjanScc tarjan(this);
1590+
1591+
// 2. For each SCC calculate: sum of refcounts - sum of internal refs - (sum of incoming refs from previous SCCs which could be freed).
1592+
// If this count is > 0, there are references from outside of gdscript (e.g. user code) or other scripts that are not dependencies of this script.
1593+
1594+
TightLocalVector<int> counts;
1595+
counts.resize_initialized(tarjan.sccs.size());
1596+
1597+
// Iterate backwards to start with the scc which has no incoming refs from other SCCs and which contains this node. It is the root to unwinding this mess.
1598+
for (int i = tarjan.sccs.size() - 1; i >= 0; i--) {
1599+
RBMap<GDScript *, int> dependencies;
1600+
for (GDScript *scr : tarjan.sccs[i]) {
1601+
counts[i] += scr->get_reference_count();
1602+
1603+
if (GDScriptCache::get_cached_script(scr->path) == scr) {
1604+
// The GDScriptCache references the script, but that's no reason to keep it alive.
1605+
// An additional ref is hold for static scripts which is unaffected by this.
1606+
counts[i] -= 1;
1607+
}
1608+
1609+
scr->count_dependencies(dependencies);
1610+
}
1611+
1612+
for (KeyValue<GDScript *, int> &dependency : dependencies) {
1613+
// The script is a transitive dep of this script so it was visited during SCC detection. It's present in the index.
1614+
if (tarjan.get_scc(dependency.key) == i) {
1615+
// Script is part of this SCC. Decrease refcount by internal refs.
1616+
counts[i] -= dependency.value;
1617+
}
1618+
}
1619+
1620+
if (counts[i] > 0) {
1621+
// There are external refs. This SCC can not be freed.
1622+
continue;
1623+
}
1624+
1625+
// Remove references from this SCC.
1626+
for (KeyValue<GDScript *, int> &dependency : dependencies) {
1627+
int scc = tarjan.get_scc(dependency.key);
1628+
if (scc != i) {
1629+
counts[scc] -= dependency.value;
1630+
}
1631+
}
1632+
1633+
for (GDScript *scr : tarjan.sccs[i]) {
1634+
clear_data->scripts.insert(scr);
1635+
scr->clear(clear_data);
1636+
}
15801637
}
15811638
}
15821639

@@ -1585,15 +1642,6 @@ void GDScript::clear(ClearData *p_clear_data) {
15851642
}
15861643
member_functions.clear();
15871644

1588-
for (KeyValue<StringName, MemberInfo> &E : member_indices) {
1589-
clear_data->scripts.insert(E.value.data_type.script_type_ref);
1590-
E.value.data_type.script_type_ref = Ref<Script>();
1591-
}
1592-
1593-
for (KeyValue<StringName, MemberInfo> &E : static_variables_indices) {
1594-
clear_data->scripts.insert(E.value.data_type.script_type_ref);
1595-
E.value.data_type.script_type_ref = Ref<Script>();
1596-
}
15971645
static_variables.clear();
15981646
static_variables_indices.clear();
15991647

@@ -1612,7 +1660,7 @@ void GDScript::clear(ClearData *p_clear_data) {
16121660
static_initializer = nullptr;
16131661
}
16141662

1615-
_save_orphaned_subclasses(clear_data);
1663+
_save_orphaned_subclasses();
16161664

16171665
#ifdef TOOLS_ENABLED
16181666
// Clearing inner class doc, script doc only cleared when the script source deleted.

0 commit comments

Comments
 (0)