Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 5 files). Show all
Unified · Split
| @@ -1496,67 +1496,58 @@ const auto RETRY_ATTEMPT_BATCH = 100; //glm::clamp(qgetenv("RETRY_ATTEMPT_BATCH" · 1495 unmodified lines | ||
| 1496 | 1496 | const auto RETRY_ATTEMPT_MSECS = 250; //glm::clamp(qgetenv("RETRY_ATTEMPT_MSECS").toInt(), 50, 1000); |
| 1497 | 1497 | const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" }; |
| 1498 | 1498 | |
| 1499 | −void ScriptEngine::deferLoadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { | |
| 1500 | − const auto needTimer = _pendingLoadEntities.isEmpty(); | |
| 1501 | − // NOTE: forceRedownload isn't forwarded here because deferLoadEntityScript() is only ever called when another entity has taken lead for the script | |
| 1502 | − _pendingLoadEntities.push_back({ entityID, entityScript/*, forceRedownload*/ }); | |
| 1503 | − | |
| 1504 | − auto processDeferredEntities = [this](QScriptContext* context, QScriptEngine*) -> QScriptValue { | |
| 1505 | − QList<DeferredLoadEntity> stillPending; | |
| 1506 | − | |
| 1507 | − for (int i=0; i < RETRY_ATTEMPT_BATCH && !_pendingLoadEntities.isEmpty(); i++) { | |
| 1508 | − auto retry = _pendingLoadEntities.takeFirst(); | |
| 1509 | − qCDebug(scriptengine) << "deferLoadEntityScript.retry[" << i << "]:" << retry.entityID << retry.entityScript; | |
| 1510 | − if (!_entityScripts.contains(retry.entityID)) { | |
| 1511 | − qCDebug(scriptengine) << "deferLoadEntityScript.retry -- entity details gone (entity deleted?)"; | |
| 1512 | − } else { | |
| 1513 | − auto details = _entityScripts[retry.entityID]; | |
| 1514 | − if (details.status != EntityScriptStatus::PENDING) { | |
| 1515 | − qCDebug(scriptengine) << "deferLoadEntityScript.retry -- entity status no longer PENDING; " << details.status; | |
| 1516 | − } else { | |
| 1517 | − if (!_occupiedScriptURLs.contains(retry.entityScript)) { | |
| 1518 | − // the URL is no longer busy with another Entity, so now it's our turn | |
| 1519 | − loadEntityScript(retry.entityID, retry.entityScript, false); | |
| 1520 | − } else { | |
| 1521 | − auto entityID = _occupiedScriptURLs[retry.entityScript]; | |
| 1522 | − if (entityID == BAD_SCRIPT_UUID_PLACEHOLDER) { | |
| 1523 | − // the URL was marked bad by an earlier load attempt, so abort the retry mission | |
| 1524 | − qCDebug(scriptengine) << "... pending load of " << retry.entityID << " cancelled"; | |
| 1525 | − } else { | |
| 1526 | − // the URL is still occupied by another Entity, so add to next retry pass | |
| 1527 | − stillPending << retry; | |
| 1528 | − } | |
| 1529 | − } | |
| 1530 | − } | |
| 1531 | − } | |
| 1532 | − } | |
| 1533 | − foreach(const DeferredLoadEntity& retry, stillPending) { | |
| 1534 | − _pendingLoadEntities.push_back(retry); | |
| 1499 | +void ScriptEngine::processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID) { | |
| 1500 | + QList<DeferredLoadEntity> retryLoads; | |
| 1501 | + QMutableListIterator<DeferredLoadEntity> i(_deferredEntityLoads); | |
| 1502 | + while (i.hasNext()) { | |
| 1503 | + auto retry = i.next(); | |
| 1504 | + if (retry.entityScript == entityScript) { | |
| 1505 | + retryLoads << retry; | |
| 1506 | + i.remove(); | |
| 1535 | 1507 | } |
| 1536 | − if (_pendingLoadEntities.isEmpty()) { | |
| 1537 | − QObject *interval = context->callee().property("interval").toQObject(); | |
| 1538 | − // we're done processing so can kill the timer | |
| 1539 | − qCDebug(scriptengine) << "loadEntityScript.retry queue processing complete -- exiting" << interval; | |
| 1540 | − if (interval) { | |
| 1541 | − clearInterval(interval); | |
| 1542 | − } | |
| 1508 | + } | |
| 1509 | + foreach(auto &retry, retryLoads) { | |
| 1510 | + // check whether entity was since been deleted | |
| 1511 | + if (!_entityScripts.contains(retry.entityID)) { | |
| 1512 | + qCDebug(scriptengine) << "processDeferredEntityLoads -- entity details gone (entity deleted?)" | |
| 1513 | + << retry.entityID; | |
| 1514 | + continue; | |
| 1543 | 1515 | } |
| 1544 | − return QScriptValue(); | |
| 1545 | − }; | |
| 1546 | 1516 | |
| 1547 | − if (needTimer) { | |
| 1548 | − // we were the first to get queued -- so set up an interval timer for deferred processing | |
| 1549 | − qCDebug(scriptengine) << "deferLoadEntityScript -- scheduling timer " << RETRY_ATTEMPT_MSECS; | |
| 1517 | + // check whether entity has since been unloaded or otherwise errored-out | |
| 1518 | + auto details = _entityScripts[retry.entityID]; | |
| 1519 | + if (details.status != EntityScriptStatus::PENDING) { | |
| 1520 | + qCDebug(scriptengine) << "processDeferredEntityLoads -- entity status no longer PENDING; " | |
| 1521 | + << retry.entityID << details.status; | |
| 1522 | + continue; | |
| 1523 | + } | |
| 1550 | 1524 | |
| 1551 | − // processDeferredEntities gets wrapped as a "regular" JavaScript callback so that it can then | |
| 1552 | − // be scheduled using setInterval per usual -- allowing the script engine to own its lifecycle. | |
| 1553 | − // we stil need to call clearInterval manually when done, so its passed to callee() above as a property. | |
| 1525 | + // propagate leader's failure reasons to the pending entity | |
| 1526 | + const auto leaderDetails = _entityScripts[leaderID]; | |
| 1527 | + if (leaderDetails.status != EntityScriptStatus::RUNNING) { | |
| 1528 | + qCDebug(scriptengine) << QString("... pending load of %1 cancelled (leader: %2 status: %3)") | |
| 1529 | + .arg(retry.entityID.toString()).arg(leaderID.toString()).arg(leaderDetails.status); | |
| 1530 | + | |
| 1531 | + auto extraDetail = "\n(propagated from " + leaderID.toString() + ")"; | |
| 1532 | + if (leaderDetails.status == EntityScriptStatus::ERROR_LOADING_SCRIPT || | |
| 1533 | + leaderDetails.status == EntityScriptStatus::ERROR_RUNNING_SCRIPT) { | |
| 1534 | + // propagate same error so User doesn't have to hunt down stampede's leader | |
| 1535 | + updateEntityScriptStatus(retry.entityID, leaderDetails.status, leaderDetails.errorInfo + extraDetail); | |
| 1536 | + } else { | |
| 1537 | + // the leader Entity somehow ended up in some other state (rapid-fire delete or unload could cause) | |
| 1538 | + updateEntityScriptStatus(retry.entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT, | |
| 1539 | + "A previous Entity failed to load using this script URL; reload to try again." + extraDetail); | |
| 1540 | + } | |
| 1541 | + continue; | |
| 1542 | + } | |
| 1554 | 1543 | |
| 1555 | − QScriptValue function = newLambdaFunction(processDeferredEntities, "processDeferredEntities-loader"); | |
| 1556 | − QObject *interval = setInterval( function, RETRY_ATTEMPT_MSECS ); | |
| 1544 | + if (_occupiedScriptURLs.contains(retry.entityScript)) { | |
| 1545 | + qCWarning(scriptengine) << "--- SHOULD NOT HAPPEN -- recursive call into processDeferredEntityLoads" << retry.entityScript; | |
| 1546 | + continue; | |
| 1547 | + } | |
| 1557 | 1548 | |
| 1558 | − // make the interval reachable from the handler | |
| 1559 | − function.setProperty("interval", newQObject(interval)); | |
| 1549 | + // if we made it here then the leading entity was successful so proceed with normal load | |
| 1550 | + loadEntityScript(retry.entityID, retry.entityScript, false); | |
| 1560 | 1551 | } |
| 1561 | 1552 | } |
| 1562 | 1553 | |
| @@ -1587,28 +1578,33 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 24 unmodified lines | ||
| 1587 | 1578 | // This "occupied" approach allows multiple Entities to boot from the same script URL while still taking |
| 1588 | 1579 | // full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back |
| 1589 | 1580 | // before the first one has time to finish loading. |
| 1590 | − if (!_occupiedScriptURLs.contains(entityScript)) { | |
| 1591 | − // the scriptURL is available -- flag as in-use and proceed | |
| 1592 | − _occupiedScriptURLs[entityScript] = entityID; | |
| 1593 | − } else { | |
| 1594 | − // another Entity is either still booting from this script URL or has failed and marked it "out of order" | |
| 1581 | + if (_occupiedScriptURLs.contains(entityScript)) { | |
| 1595 | 1582 | auto currentEntityID = _occupiedScriptURLs[entityScript]; |
| 1596 | − if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER && !forceRedownload) { | |
| 1597 | − // since not force-reloading, assume that the exact same input would produce the exact same output again | |
| 1598 | − // note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc. | |
| 1599 | − qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)") | |
| 1600 | − .arg(entityID.toString()).arg(entityScript); | |
| 1601 | − updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT, | |
| 1602 | − "Cached Script was marked invalid by previous Entity load failure."); | |
| 1583 | + if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER) { | |
| 1584 | + if (forceRedownload) { | |
| 1585 | + // script was previously marked unusable, but we're reloading so reset it | |
| 1586 | + _occupiedScriptURLs.remove(entityScript); | |
| 1587 | + } else { | |
| 1588 | + // since not reloading, assume that the exact same input would produce the exact same output again | |
| 1589 | + // note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc. | |
| 1590 | + qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)") | |
| 1591 | + .arg(entityID.toString()).arg(entityScript); | |
| 1592 | + updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT, | |
| 1593 | + "A previous Entity failed to load using this script URL; reload to try again."); | |
| 1594 | + return; | |
| 1595 | + } | |
| 1603 | 1596 | } else { |
| 1604 | 1597 | // another entity is busy loading from this script URL so wait for them to finish |
| 1605 | − qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2") | |
| 1606 | − .arg( _pendingLoadEntities.size()).arg(entityID.toString()).arg(entityScript); | |
| 1607 | − deferLoadEntityScript(entityID, entityScript, forceRedownload); | |
| 1598 | + qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2 (waiting on %3)") | |
| 1599 | + .arg( _deferredEntityLoads.size()).arg(entityID.toString()).arg(entityScript).arg(currentEntityID.toString()); | |
| 1600 | + _deferredEntityLoads.push_back({ entityID, entityScript }); | |
| 1601 | + return; | |
| 1608 | 1602 | } |
| 1609 | − return; | |
| 1610 | 1603 | } |
| 1611 | 1604 | |
| 1605 | + // the scriptURL slot is available; flag as in-use | |
| 1606 | + _occupiedScriptURLs[entityScript] = entityID; | |
| 1607 | + | |
| 1612 | 1608 | #ifdef DEBUG_ENTITY_STATES |
| 1613 | 1609 | auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING; |
| 1614 | 1610 | qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString() |
| @@ -1642,7 +1638,7 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 27 unmodified lines | ||
| 1642 | 1638 | #endif |
| 1643 | 1639 | } |
| 1644 | 1640 | // recheck whether us since may have been set to BAD_SCRIPT_UUID_PLACEHOLDER in entityScriptContentAvailable |
| 1645 | − if (_occupiedScriptURLs[entityScript] == entityID) { | |
| 1641 | + if (_occupiedScriptURLs.contains(entityScript) && _occupiedScriptURLs[entityScript] == entityID) { | |
| 1646 | 1642 | _occupiedScriptURLs.remove(entityScript); |
| 1647 | 1643 | } |
| 1648 | 1644 | }); |
| @@ -1679,6 +1675,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 30 unmodified lines | ||
| 1679 | 1675 | auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript"; |
| 1680 | 1676 | |
| 1681 | 1677 | const EntityScriptDetails &oldDetails = _entityScripts[entityID]; |
| 1678 | + const auto entityScript = oldDetails.scriptText; | |
| 1679 | + | |
| 1682 | 1680 | EntityScriptDetails newDetails; |
| 1683 | 1681 | newDetails.scriptText = scriptOrURL; |
| 1684 | 1682 | |
| @@ -1689,10 +1687,10 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 4 unmodified lines | ||
| 1689 | 1687 | newDetails.status = status; |
| 1690 | 1688 | setEntityScriptDetails(entityID, newDetails); |
| 1691 | 1689 | |
| 1692 | − qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << oldDetails.scriptText << " as BAD_SCRIPT_UUID_PLACEHOLDER"; | |
| 1693 | − // flag this scriptURL as unusuable | |
| 1694 | − // note: we want the *original* entityScript string (not the potentially normalized one from ScriptCache results) | |
| 1695 | − _occupiedScriptURLs[oldDetails.scriptText] = BAD_SCRIPT_UUID_PLACEHOLDER; | |
| 1690 | + qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << entityScript << " as BAD_SCRIPT_UUID_PLACEHOLDER"; | |
| 1691 | + // flag the original entityScript as unusuable | |
| 1692 | + _occupiedScriptURLs[entityScript] = BAD_SCRIPT_UUID_PLACEHOLDER; | |
| 1693 | + processDeferredEntityLoads(entityScript, entityID); | |
| 1696 | 1694 | }; |
| 1697 | 1695 | |
| 1698 | 1696 | // NETWORK / FILESYSTEM ERRORS |
| @@ -1826,6 +1824,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 127 unmodified lines | ||
| 1826 | 1824 | |
| 1827 | 1825 | // if we got this far, then call the preload method |
| 1828 | 1826 | callEntityScriptMethod(entityID, "preload"); |
| 1827 | + | |
| 1828 | + _occupiedScriptURLs.remove(entityScript); | |
| 1829 | + processDeferredEntityLoads(entityScript, entityID); | |
| 1829 | 1830 | } |
| 1830 | 1831 | |
| 1831 | 1832 | void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { |
| @@ -1845,14 +1846,25 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { · 13 unmodified lines | ||
| 1845 | 1846 | #endif |
| 1846 | 1847 | |
| 1847 | 1848 | if (_entityScripts.contains(entityID)) { |
| 1849 | + auto oldDetails = _entityScripts[entityID]; | |
| 1848 | 1850 | if (isEntityScriptRunning(entityID)) { |
| 1849 | 1851 | callEntityScriptMethod(entityID, "unload"); |
| 1852 | + } else { | |
| 1853 | + qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; | |
| 1854 | + } | |
| 1855 | + if (oldDetails.status != EntityScriptStatus::UNLOADED) { | |
| 1856 | + EntityScriptDetails newDetails; | |
| 1857 | + newDetails.status = EntityScriptStatus::UNLOADED; | |
| 1858 | + newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); | |
| 1859 | + // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript | |
| 1860 | + newDetails.scriptText = oldDetails.scriptText; | |
| 1861 | + setEntityScriptDetails(entityID, newDetails); | |
| 1850 | 1862 | } |
| 1851 | − EntityScriptDetails newDetails; | |
| 1852 | − newDetails.status = EntityScriptStatus::UNLOADED; | |
| 1853 | − newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); | |
| 1854 | − setEntityScriptDetails(entityID, newDetails); | |
| 1855 | 1863 | stopAllTimersForEntityScript(entityID); |
| 1864 | + { | |
| 1865 | + // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests | |
| 1866 | + processDeferredEntityLoads(oldDetails.scriptText, entityID); | |
| 1867 | + } | |
| 1856 | 1868 | } |
| 1857 | 1869 | } |
| 1858 | 1870 | |
| @@ -1902,17 +1914,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { · 43 unmodified lines | ||
| 1902 | 1914 | auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch(); |
| 1903 | 1915 | if (lastModified > details.lastModified) { |
| 1904 | 1916 | scriptInfoMessage("Reloading modified script " + details.scriptText); |
| 1905 | − | |
| 1906 | − QFile file(filePath); | |
| 1907 | − file.open(QIODevice::ReadOnly); | |
| 1908 | − QString scriptContents = QTextStream(&file).readAll(); | |
| 1909 | − this->unloadEntityScript(entityID); | |
| 1910 | − this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true, "Success"); | |
| 1911 | − if (!isEntityScriptRunning(entityID)) { | |
| 1912 | − scriptWarningMessage("Reload script " + details.scriptText + " failed"); | |
| 1913 | − } else { | |
| 1914 | − details = _entityScripts[entityID]; | |
| 1915 | − } | |
| 1917 | + loadEntityScript(entityID, details.scriptText, true); | |
| 1916 | 1918 | } |
| 1917 | 1919 | } |
| 1918 | 1920 | recurseGuard = false; |