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 | const auto RETRY_ATTEMPT_MSECS = 250; //glm::clamp(qgetenv("RETRY_ATTEMPT_MSECS").toInt(), 50, 1000); | 1496 | const auto RETRY_ATTEMPT_MSECS = 250; //glm::clamp(qgetenv("RETRY_ATTEMPT_MSECS").toInt(), 50, 1000); |
| 1497 | const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" }; | 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) { | 1499 | void ScriptEngine::processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID) { |
| 1500 | const auto needTimer = _pendingLoadEntities.isEmpty(); | 1500 | QList<DeferredLoadEntity> retryLoads; |
| 1501 | // NOTE: forceRedownload isn't forwarded here because deferLoadEntityScript() is only ever called when another entity has taken lead for the script | 1501 | QMutableListIterator<DeferredLoadEntity> i(_deferredEntityLoads); |
| 1502 | _pendingLoadEntities.push_back({ entityID, entityScript/*, forceRedownload*/ }); | 1502 | while (i.hasNext()) { |
| 1503 | 1503 | auto retry = i.next(); | |
| 1504 | auto processDeferredEntities = [this](QScriptContext* context, QScriptEngine*) -> QScriptValue { | 1504 | if (retry.entityScript == entityScript) { |
| 1505 | QList<DeferredLoadEntity> stillPending; | 1505 | retryLoads << retry; |
| 1506 | 1506 | i.remove(); | |
| 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); | ||
| 1535 | } | 1507 | } |
| 1536 | if (_pendingLoadEntities.isEmpty()) { | 1508 | } |
| 1537 | QObject *interval = context->callee().property("interval").toQObject(); | 1509 | foreach(auto &retry, retryLoads) { |
| 1538 | // we're done processing so can kill the timer | 1510 | // check whether entity was since been deleted |
| 1539 | qCDebug(scriptengine) << "loadEntityScript.retry queue processing complete -- exiting" << interval; | 1511 | if (!_entityScripts.contains(retry.entityID)) { |
| 1540 | if (interval) { | 1512 | qCDebug(scriptengine) << "processDeferredEntityLoads -- entity details gone (entity deleted?)" |
| 1541 | clearInterval(interval); | 1513 | << retry.entityID; |
| 1542 | } | 1514 | continue; |
| 1543 | } | 1515 | } |
| 1544 | return QScriptValue(); | ||
| 1545 | }; | ||
| 1546 | 1516 | ||
| 1547 | if (needTimer) { | 1517 | // check whether entity has since been unloaded or otherwise errored-out |
| 1548 | // we were the first to get queued -- so set up an interval timer for deferred processing | 1518 | auto details = _entityScripts[retry.entityID]; |
| 1549 | qCDebug(scriptengine) << "deferLoadEntityScript -- scheduling timer " << RETRY_ATTEMPT_MSECS; | 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 | 1525 | // propagate leader's failure reasons to the pending entity |
| 1552 | // be scheduled using setInterval per usual -- allowing the script engine to own its lifecycle. | 1526 | const auto leaderDetails = _entityScripts[leaderID]; |
| 1553 | // we stil need to call clearInterval manually when done, so its passed to callee() above as a property. | 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"); | 1544 | if (_occupiedScriptURLs.contains(retry.entityScript)) { |
| 1556 | QObject *interval = setInterval( function, RETRY_ATTEMPT_MSECS ); | 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 | 1549 | // if we made it here then the leading entity was successful so proceed with normal load |
| 1559 | function.setProperty("interval", newQObject(interval)); | 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 | // This "occupied" approach allows multiple Entities to boot from the same script URL while still taking | 1578 | // This "occupied" approach allows multiple Entities to boot from the same script URL while still taking |
| 1588 | // full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back | 1579 | // full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back |
| 1589 | // before the first one has time to finish loading. | 1580 | // before the first one has time to finish loading. |
| 1590 | if (!_occupiedScriptURLs.contains(entityScript)) { | 1581 | 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" | ||
| 1595 | auto currentEntityID = _occupiedScriptURLs[entityScript]; | 1582 | auto currentEntityID = _occupiedScriptURLs[entityScript]; |
| 1596 | if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER && !forceRedownload) { | 1583 | if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER) { |
| 1597 | // since not force-reloading, assume that the exact same input would produce the exact same output again | 1584 | if (forceRedownload) { |
| 1598 | // note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc. | 1585 | // script was previously marked unusable, but we're reloading so reset it |
| 1599 | qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)") | 1586 | _occupiedScriptURLs.remove(entityScript); |
| 1600 | .arg(entityID.toString()).arg(entityScript); | 1587 | } else { |
| 1601 | updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT, | 1588 | // since not reloading, assume that the exact same input would produce the exact same output again |
| 1602 | "Cached Script was marked invalid by previous Entity load failure."); | 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 | } else { | 1596 | } else { |
| 1604 | // another entity is busy loading from this script URL so wait for them to finish | 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") | 1598 | qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2 (waiting on %3)") |
| 1606 | .arg( _pendingLoadEntities.size()).arg(entityID.toString()).arg(entityScript); | 1599 | .arg( _deferredEntityLoads.size()).arg(entityID.toString()).arg(entityScript).arg(currentEntityID.toString()); |
| 1607 | deferLoadEntityScript(entityID, entityScript, forceRedownload); | 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 | #ifdef DEBUG_ENTITY_STATES | 1608 | #ifdef DEBUG_ENTITY_STATES |
| 1613 | auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING; | 1609 | auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING; |
| 1614 | qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString() | 1610 | qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString() |
| @@ -1642,7 +1638,7 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 27 unmodified lines | |||
| 1642 | #endif | 1638 | #endif |
| 1643 | } | 1639 | } |
| 1644 | // recheck whether us since may have been set to BAD_SCRIPT_UUID_PLACEHOLDER in entityScriptContentAvailable | 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 | _occupiedScriptURLs.remove(entityScript); | 1642 | _occupiedScriptURLs.remove(entityScript); |
| 1647 | } | 1643 | } |
| 1648 | }); | 1644 | }); |
| @@ -1679,6 +1675,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 30 unmodified lines | |||
| 1679 | auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript"; | 1675 | auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript"; |
| 1680 | 1676 | ||
| 1681 | const EntityScriptDetails &oldDetails = _entityScripts[entityID]; | 1677 | const EntityScriptDetails &oldDetails = _entityScripts[entityID]; |
| 1678 | const auto entityScript = oldDetails.scriptText; | ||
| 1679 | |||
| 1682 | EntityScriptDetails newDetails; | 1680 | EntityScriptDetails newDetails; |
| 1683 | newDetails.scriptText = scriptOrURL; | 1681 | newDetails.scriptText = scriptOrURL; |
| 1684 | 1682 | ||
| @@ -1689,10 +1687,10 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 4 unmodified lines | |||
| 1689 | newDetails.status = status; | 1687 | newDetails.status = status; |
| 1690 | setEntityScriptDetails(entityID, newDetails); | 1688 | setEntityScriptDetails(entityID, newDetails); |
| 1691 | 1689 | ||
| 1692 | qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << oldDetails.scriptText << " as BAD_SCRIPT_UUID_PLACEHOLDER"; | 1690 | qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << entityScript << " as BAD_SCRIPT_UUID_PLACEHOLDER"; |
| 1693 | // flag this scriptURL as unusuable | 1691 | // flag the original entityScript as unusuable |
| 1694 | // note: we want the *original* entityScript string (not the potentially normalized one from ScriptCache results) | 1692 | _occupiedScriptURLs[entityScript] = BAD_SCRIPT_UUID_PLACEHOLDER; |
| 1695 | _occupiedScriptURLs[oldDetails.scriptText] = BAD_SCRIPT_UUID_PLACEHOLDER; | 1693 | processDeferredEntityLoads(entityScript, entityID); |
| 1696 | }; | 1694 | }; |
| 1697 | 1695 | ||
| 1698 | // NETWORK / FILESYSTEM ERRORS | 1696 | // NETWORK / FILESYSTEM ERRORS |
| @@ -1826,6 +1824,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 127 unmodified lines | |||
| 1826 | 1824 | ||
| 1827 | // if we got this far, then call the preload method | 1825 | // if we got this far, then call the preload method |
| 1828 | callEntityScriptMethod(entityID, "preload"); | 1826 | callEntityScriptMethod(entityID, "preload"); |
| 1827 | |||
| 1828 | _occupiedScriptURLs.remove(entityScript); | ||
| 1829 | processDeferredEntityLoads(entityScript, entityID); | ||
| 1829 | } | 1830 | } |
| 1830 | 1831 | ||
| 1831 | void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { | 1832 | void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { |
| @@ -1845,14 +1846,25 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { · 13 unmodified lines | |||
| 1845 | #endif | 1846 | #endif |
| 1846 | 1847 | ||
| 1847 | if (_entityScripts.contains(entityID)) { | 1848 | if (_entityScripts.contains(entityID)) { |
| 1849 | auto oldDetails = _entityScripts[entityID]; | ||
| 1848 | if (isEntityScriptRunning(entityID)) { | 1850 | if (isEntityScriptRunning(entityID)) { |
| 1849 | callEntityScriptMethod(entityID, "unload"); | 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 | stopAllTimersForEntityScript(entityID); | 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 | auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch(); | 1914 | auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch(); |
| 1903 | if (lastModified > details.lastModified) { | 1915 | if (lastModified > details.lastModified) { |
| 1904 | scriptInfoMessage("Reloading modified script " + details.scriptText); | 1916 | scriptInfoMessage("Reloading modified script " + details.scriptText); |
| 1905 | 1917 | loadEntityScript(entityID, details.scriptText, true); | |
| 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 | } | ||
| 1916 | } | 1918 | } |
| 1917 | } | 1919 | } |
| 1918 | recurseGuard = false; | 1920 | recurseGuard = false; |