History

* Switch trigger for deferred loads from timer to using leading entity completion * Bring back notion of tracking "bad scripts" to fail stampeding entities as a set * Update stampede JS tests to highlight/troubleshoot preexisting unload issue

humbletim committed 9 years ago

Commit 3ac11651 parent 02827555 files · +128 −96Browse files

Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 5 files). Show all

Unified · Split

libraries/script-engine/src/ScriptEngine.cpp+93−91
@@ -1496,67 +1496,58 @@ const auto RETRY_ATTEMPT_BATCH = 100; //glm::clamp(qgetenv("RETRY_ATTEMPT_BATCH" · 1495 unmodified lines
14961496 const auto RETRY_ATTEMPT_MSECS = 250; //glm::clamp(qgetenv("RETRY_ATTEMPT_MSECS").toInt(), 50, 1000);
14971497 const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" };
14981498
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();
15351507 }
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;
15431515 }
1544− return QScriptValue();
1545− };
15461516
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+ }
15501524
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+ }
15541543
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+ }
15571548
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);
15601551 }
15611552 }
15621553
@@ -1587,28 +1578,33 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 24 unmodified lines
15871578 // This "occupied" approach allows multiple Entities to boot from the same script URL while still taking
15881579 // full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back
15891580 // 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)) {
15951582 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+ }
16031596 } else {
16041597 // 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;
16081602 }
1609− return;
16101603 }
16111604
1605+ // the scriptURL slot is available; flag as in-use
1606+ _occupiedScriptURLs[entityScript] = entityID;
1607+
16121608 #ifdef DEBUG_ENTITY_STATES
16131609 auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING;
16141610 qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString()
@@ -1642,7 +1638,7 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 27 unmodified lines
16421638 #endif
16431639 }
16441640 // 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) {
16461642 _occupiedScriptURLs.remove(entityScript);
16471643 }
16481644 });
@@ -1679,6 +1675,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 30 unmodified lines
16791675 auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript";
16801676
16811677 const EntityScriptDetails &oldDetails = _entityScripts[entityID];
1678+ const auto entityScript = oldDetails.scriptText;
1679+
16821680 EntityScriptDetails newDetails;
16831681 newDetails.scriptText = scriptOrURL;
16841682
@@ -1689,10 +1687,10 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 4 unmodified lines
16891687 newDetails.status = status;
16901688 setEntityScriptDetails(entityID, newDetails);
16911689
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);
16961694 };
16971695
16981696 // NETWORK / FILESYSTEM ERRORS
@@ -1826,6 +1824,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 127 unmodified lines
18261824
18271825 // if we got this far, then call the preload method
18281826 callEntityScriptMethod(entityID, "preload");
1827+
1828+ _occupiedScriptURLs.remove(entityScript);
1829+ processDeferredEntityLoads(entityScript, entityID);
18291830 }
18301831
18311832 void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
@@ -1845,14 +1846,25 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { · 13 unmodified lines
18451846 #endif
18461847
18471848 if (_entityScripts.contains(entityID)) {
1849+ auto oldDetails = _entityScripts[entityID];
18481850 if (isEntityScriptRunning(entityID)) {
18491851 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);
18501862 }
1851− EntityScriptDetails newDetails;
1852− newDetails.status = EntityScriptStatus::UNLOADED;
1853− newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();
1854− setEntityScriptDetails(entityID, newDetails);
18551863 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+ }
18561868 }
18571869 }
18581870
@@ -1902,17 +1914,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { · 43 unmodified lines
19021914 auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();
19031915 if (lastModified > details.lastModified) {
19041916 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);
19161918 }
19171919 }
19181920 recurseGuard = false;