diff --git a/sirepo/package_data/static/js/openmc.js b/sirepo/package_data/static/js/openmc.js
index 5257a508bc..9c200a17f0 100644
--- a/sirepo/package_data/static/js/openmc.js
+++ b/sirepo/package_data/static/js/openmc.js
@@ -85,9 +85,7 @@ SIREPO.app.config(() => {
@@ -662,10 +660,8 @@ SIREPO.app.factory('tallyService', function(appState, openmcService, utilities,
self.updateTallyDisplay = () => {
appState.models.tallyReport.colorMap = appState.models.openmcAnimation.colorMap;
- // save quietly but immediately
appState.saveQuietly('openmcAnimation');
appState.saveQuietly('tallyReport');
- appState.autoSave();
};
$rootScope.$on('modelsUnloaded', self.clearMesh);
@@ -796,6 +792,29 @@ SIREPO.app.directive('planeList', function(appState) {
};
});
+SIREPO.app.directive('plotScoreList', function(openmcService) {
+ return {
+ restrict: 'A',
+ scoe: {
+ model: '=',
+ field: '=',
+ },
+ template: `
+
+
+
+ `,
+ controller: function($scope) {
+ function buildScores() {
+ const t = openmcService.findTally();
+ $scope.scores = t ? t.scores.map((s) => s.score) : [];
+ }
+ buildScores();
+ $scope.$watch('model.tally', buildScores);
+ },
+ };
+});
+
SIREPO.app.directive('tallyVolumePicker', function(openmcService, volumeLoadingService) {
return {
restrict: 'A',
@@ -1205,7 +1224,7 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState,
function updateDisplay() {
tallyService.updateTallyDisplay();
- updateSlice();
+ buildTallyReport();
}
function updateDisplayRange() {
@@ -1215,18 +1234,9 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState,
SIREPO.GEOMETRY.GeometryUtils.BASIS().forEach(dim => {
displayRanges[dim] = tallyService.tallyRange(dim);
});
- updateVisibleAxes();
updateSliceAxis();
}
- function updateSlice() {
- buildTallyReport();
- // save quietly but immediately
- appState.saveQuietly('tallyReport');
- appState.saveQuietly('openmcAnimation');
- appState.autoSave();
- }
-
function updateSliceAxis() {
if (! tallyService.fieldData) {
return;
@@ -1234,26 +1244,7 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState,
if (! tallyService.initMesh()) {
return ;
}
- updateSlice();
- }
-
- function updateVisibleAxes() {
- const v = {};
- SIREPO.GEOMETRY.GeometryUtils.BASIS().forEach(dim => {
- v[dim] = true;
- SIREPO.GEOMETRY.GeometryUtils.BASIS_VECTORS()[dim].forEach((bv, bi) => {
- if (! bv && tallyService.mesh.dimension[bi] < SIREPO.APP_SCHEMA.constants.minTallyResolution) {
- delete v[dim];
- }
- });
- });
- SIREPO.GEOMETRY.GeometryUtils.BASIS().forEach(dim => {
- const s = ! Object.keys(v).length || dim in v;
- panelState.showEnum('tallyReport', 'axis', dim, s);
- if (! s && appState.models.tallyReport.axis === dim) {
- appState.models.tallyReport.axis = Object.keys(v)[0];
- }
- });
+ buildTallyReport();
}
function vectorScaleFactor() {
@@ -1282,7 +1273,7 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState,
$scope.$on('sr-volume-visibility-toggle-all', buildTallyReport);
appState.watchModelFields($scope, ['tallyReport.axis'], updateSliceAxis);
appState.watchModelFields($scope, ['openmcAnimation.colorMap', 'openmcAnimation.sourceColorMap'], updateDisplay);
- appState.watchModelFields($scope, ['tallyReport.planePos', 'openmcAnimation.showSources'], updateSlice, true);
+ appState.watchModelFields($scope, ['tallyReport.planePos', 'openmcAnimation.showSources'], buildTallyReport);
$scope.$watch('tallyService.fieldData', (newValue, oldValue) => {
if (newValue && newValue !== oldValue) {
updateDisplayRange();
@@ -1827,8 +1818,6 @@ SIREPO.app.directive('geometry3d', function(appState, openmcService, plotting, p
function showSources() {
addSources();
- appState.saveQuietly($scope.modelName);
- appState.autoSave();
}
const updateDisplay = utilities.debounce(() => {
@@ -1860,8 +1849,11 @@ SIREPO.app.directive('geometry3d', function(appState, openmcService, plotting, p
`${$scope.modelName}.colorMap`,
`${$scope.modelName}.opacity`,
`${$scope.modelName}.sourceColorMap`,
- ], updateDisplay, true);
- appState.watchModelFields($scope, [`${$scope.modelName}.showSources`], showSources, true);
+ ], updateDisplay);
+ appState.watchModelFields($scope, [
+ `${$scope.modelName}.opacity`,
+ ], setGlobalProperties);
+ appState.watchModelFields($scope, [`${$scope.modelName}.showSources`], showSources);
$scope.$watch('tallyService.fieldData', (newValue, oldValue) => {
if (vtkScene && newValue && newValue !== oldValue) {
$rootScope.$broadcast('vtk.showLoader');
@@ -3173,6 +3165,7 @@ SIREPO.viewLogic('tallySettingsView', function(appState, openmcService, panelSta
panelState.showField('openmcAnimation', 'sourceNormalization', openmcService.canNormalizeScore(appState.models.openmcAnimation.score));
panelState.showField('openmcAnimation', 'numSampleSourceParticles', showSources);
panelState.showField('openmcAnimation', 'sourceColorMap', showSources && appState.models.openmcAnimation.numSampleSourceParticles);
+ updateVisibleAxes();
}
function updateEnergyPlot() {
@@ -3180,6 +3173,28 @@ SIREPO.viewLogic('tallySettingsView', function(appState, openmcService, panelSta
appState.saveChanges('energyAnimation');
}
+ function updateVisibleAxes() {
+ if (! tallyService.mesh) {
+ return;
+ }
+ const v = {};
+ SIREPO.GEOMETRY.GeometryUtils.BASIS().forEach(dim => {
+ v[dim] = true;
+ SIREPO.GEOMETRY.GeometryUtils.BASIS_VECTORS()[dim].forEach((bv, bi) => {
+ if (! bv && tallyService.mesh.dimension[bi] < SIREPO.APP_SCHEMA.constants.minTallyResolution) {
+ delete v[dim];
+ }
+ });
+ });
+ SIREPO.GEOMETRY.GeometryUtils.BASIS().forEach(dim => {
+ const s = ! Object.keys(v).length || dim in v;
+ panelState.showEnum('tallyReport', 'axis', dim, s);
+ if (! s && appState.models.tallyReport.axis === dim) {
+ appState.models.tallyReport.axis = Object.keys(v)[0];
+ }
+ });
+ }
+
function validateTally() {
openmcService.validateSelectedTally();
autoUpdate();
diff --git a/sirepo/package_data/static/js/sirepo.js b/sirepo/package_data/static/js/sirepo.js
index 36907669c3..9107bf5e06 100644
--- a/sirepo/package_data/static/js/sirepo.js
+++ b/sirepo/package_data/static/js/sirepo.js
@@ -504,12 +504,7 @@ SIREPO.app.factory('appState', function(errorService, fileManager, msgRouter, re
});
};
- self.deepEquals = function(v1, v2, doStripHashKey=false) {
-
- function stripHashKey(keys) {
- return doStripHashKey ? keys.filter(k => k !== '$$hashKey') : keys;
- }
-
+ self.deepEquals = function(v1, v2) {
if (v1 === v2) {
return true;
}
@@ -518,18 +513,18 @@ SIREPO.app.factory('appState', function(errorService, fileManager, msgRouter, re
return false;
}
for (let i = 0; i < v1.length; i++) {
- if (! self.deepEquals(v1[i], v2[i], doStripHashKey)) {
+ if (! self.deepEquals(v1[i], v2[i])) {
return false;
}
}
return true;
}
if (angular.isObject(v1) && angular.isObject(v2)) {
- const keys = stripHashKey(Object.keys(v1));
- if (keys.length !== stripHashKey(Object.keys(v2)).length) {
+ const keys = Object.keys(v1);
+ if (keys.length !== Object.keys(v2).length) {
return false;
}
- return ! keys.some(k => ! self.deepEquals(v1[k], v2[k], doStripHashKey));
+ return ! keys.some(k => ! self.deepEquals(v1[k], v2[k]));
}
return v1 == v2;
};
@@ -775,7 +770,6 @@ SIREPO.app.factory('appState', function(errorService, fileManager, msgRouter, re
if (typeof(name) == 'string') {
name = [name];
}
- let updatedFields = [];
var updatedModels = [];
var requireReportUpdate = false;
@@ -787,12 +781,6 @@ SIREPO.app.factory('appState', function(errorService, fileManager, msgRouter, re
}
}
else {
- for (let f in self.models[name[i]]) {
- const s = savedModelValues[name[i]];
- if (! s || ! self.deepEquals(s[f], self.models[name[i]][f], true)) {
- updatedFields.push(`${name[i]}.${f}`);
- }
- }
self.saveQuietly(name[i]);
updatedModels.push(name[i]);
if (! self.isReportModelName(name[i])) {
@@ -1556,14 +1544,158 @@ SIREPO.app.service('validationService', function(utilities) {
});
+SIREPO.app.factory('srCache', function(appState, $rootScope) {
+
+ // Browser side caching implemented using indexedDB
+ // - Caches sim frame responses
+ // - Allows cache to be cleared by simId or (simId, modelName)
+ // - Keeps updateTime on item access and deletes expired records at startup
+
+ const self = {};
+ const STORE = 'db';
+ const FRAME = 'frame';
+ // 30 days until expired
+ const EXPIRY_TIME = 30 * 24 * 60 * 60 * 1000;
+ let db = null;
+
+ const deleteKeys = (keys) => {
+ if (! keys.length) {
+ return;
+ }
+ withObjectStore('readwrite', (o) => {
+ for (const k of keys) {
+ o.delete(k);
+ }
+ });
+ };
+
+ const initializeDatabase = () => {
+ if (! window.indexedDB || ! SIREPO.authState.uiWebSocket) {
+ return;
+ }
+ const r = window.indexedDB.open('srCache', 2);
+ r.onsuccess = (event) => {
+ db = event.target.result;
+ removeOldRecords();
+ };
+ r.onupgradeneeded = (event) => {
+ const d = event.target.result;
+ if (d.objectStoreNames.contains(STORE)) {
+ d.deleteObjectStore(STORE);
+ }
+ const o = d.createObjectStore(STORE);
+ o.createIndex('simId', '_srcache_simId', { unique: false });
+ o.createIndex('updateTime', '_srcache_updateTime', { unique: false });
+ };
+ };
+
+ const invokeCallback = (callback, value) => {
+ $rootScope.$applyAsync(() => callback(value));
+ };
+
+ const objectKey = (prefix, value) => prefix + ':' + value;
+
+ const getObjectStore = (mode) => {
+ // Returns null if the objectStore is not accessible
+ try {
+ if (db) {
+ return db.transaction(STORE, mode).objectStore(STORE);
+ }
+ }
+ catch (e) {
+ // at any point the browser can remove the object store
+ // and the transaction() would raise a NotFoundError
+ }
+ return null;
+ };
-SIREPO.app.factory('frameCache', function(appState, panelState, requestSender, authState, $interval, $rootScope, $timeout) {
+ const removeOldRecords = () => {
+ withObjectStore('readonly', (o) => {
+ const expired = [];
+ const d = new Date().getTime();
+ o.index('updateTime').openKeyCursor().onsuccess = (event) => {
+ const c = event.target.result;
+ if (c) {
+ if ((d - c.key) > EXPIRY_TIME) {
+ expired.push(c.primaryKey);
+ }
+ c.continue();
+ }
+ else {
+ deleteKeys(expired);
+ }
+ };
+ });
+ };
+
+ const withObjectStore = (mode, callback) => {
+ const o = getObjectStore(mode);
+ if (o) {
+ callback(o);
+ }
+ };
+
+ self.clearFrames = (simId, modelName) => {
+ // deletes frames by simId, or (simId, modelName)
+ withObjectStore('readonly', (o) => {
+ const keys = [];
+ o.index('simId').openCursor(window.IDBKeyRange.only(simId)).onsuccess = (event) => {
+ const c = event.target.result;
+ if (c) {
+ if ((! modelName) || modelName === c.value._srcache_modelName) {
+ keys.push(c.primaryKey);
+ c.continue();
+ }
+ }
+ else {
+ deleteKeys(keys);
+ }
+ };
+ });
+ };
+
+ self.getFrame = (frameId, modelName, callback) => {
+ const o = getObjectStore('readonly');
+ if (! o) {
+ invokeCallback(callback, null);
+ return;
+ }
+ const c = o.get(objectKey(FRAME, frameId));
+ c.onsuccess = (event) => {
+ const d = event.target.result;
+ invokeCallback(callback, d);
+ if (d) {
+ // sets updateTime
+ self.saveFrame(frameId, modelName, d);
+ }
+ };
+ c.onerror = () => {
+ invokeCallback(callback, null);
+ };
+ };
+
+ self.saveFrame = (frameId, modelName, data) => {
+ withObjectStore('readwrite', (o) => {
+ data._srcache_updateTime = new Date().getTime();
+ data._srcache_modelName = modelName;
+ data._srcache_simId = appState.models.simulation.simulationId;
+ o.put(data, objectKey(FRAME, frameId));
+ });
+ };
+
+ initializeDatabase();
+
+ return self;
+});
+
+
+SIREPO.app.factory('frameCache', function(appState, panelState, requestSender, srCache, $interval, $rootScope, $timeout) {
var self = {};
var frameCountByModelKey = {};
var masterFrameCount = 0;
self.modelToCurrentFrame = {};
- self.frameId = function(frameReport, frameIndex) {
+ function frameId(frameReport, frameIndex) {
function fieldToFrameParam(field) {
if (angular.isObject(field)) {
return JSON.stringify(field);
@@ -1596,7 +1728,7 @@ SIREPO.app.factory('frameCache', function(appState, panelState, requestSender, a
return v.concat(
f.map(a => fieldToFrameParam(m[a]))
).join('*');
- };
+ }
self.getCurrentFrame = function(modelName) {
return self.modelToCurrentFrame[modelName] || 0;
@@ -1641,39 +1773,51 @@ SIREPO.app.factory('frameCache', function(appState, panelState, requestSender, a
return milliseconds / parseInt(x);
};
- const requestFunction = function() {
- setPanelStateIsLoadingTimer = $timeout(() => {
- panelState.setLoading(modelName, true);
- }, 5000);
- requestSender.sendRequest(
- {
- routeName: 'simulationFrame',
- frame_id: self.frameId(modelName, index),
- },
- function(data) {
- cancelSetPanelStateIsLoadingTimer();
- panelState.setLoading(modelName, false);
- if ('state' in data && data.state === 'missing') {
- onError();
- return;
- }
- let e = framePeriod() - (now() - frameRequestTime);
- if (e <= 0) {
- callback(index, data);
- return;
- }
- $interval(
- function() {
- callback(index, data);
- },
- e,
- 1
- );
+ const callbackData = (data) => {
+ let e = framePeriod() - (now() - frameRequestTime);
+ if (e <= 0) {
+ callback(index, data);
+ return;
+ }
+ $interval(
+ function() {
+ callback(index, data);
},
- null,
- onError
+ e,
+ 1
);
};
+
+ const requestFunction = function() {
+ const id = frameId(modelName, index);
+ srCache.getFrame(id, modelName, (data) => {
+ if (data) {
+ callbackData(data);
+ return;
+ }
+ setPanelStateIsLoadingTimer = $timeout(() => {
+ panelState.setLoading(modelName, true);
+ }, 5000);
+ requestSender.sendRequest(
+ {
+ routeName: 'simulationFrame',
+ frame_id: id,
+ },
+ function(data) {
+ cancelSetPanelStateIsLoadingTimer();
+ panelState.setLoading(modelName, false);
+ if ('state' in data && data.state === 'missing') {
+ onError();
+ return;
+ }
+ callbackData(data);
+ srCache.saveFrame(id, modelName, data);
+ },
+ null,
+ onError
+ );
+ });
+ };
if (isHidden) {
panelState.addPendingRequest(modelName, requestFunction);
}
@@ -1717,6 +1861,9 @@ SIREPO.app.factory('frameCache', function(appState, panelState, requestSender, a
self.setFrameCount = function(frameCount, modelKey) {
if (modelKey) {
frameCountByModelKey[modelKey] = frameCount;
+ if (frameCount === 0) {
+ srCache.clearFrames(appState.models.simulation.simulationId, modelKey);
+ }
return;
}
if (frameCount == masterFrameCount) {
@@ -3539,7 +3686,7 @@ SIREPO.app.factory('requestQueue', function($rootScope, requestSender) {
});
-SIREPO.app.factory('persistentSimulation', function(simulationQueue, appState, authState, frameCache, stringsService, $interval) {
+SIREPO.app.factory('persistentSimulation', function(simulationQueue, appState, authState, frameCache, stringsService, srCache, $interval) {
var self = {};
const ELAPSED_TIME_INTERVAL_SECS = 1;
@@ -3758,6 +3905,7 @@ SIREPO.app.factory('persistentSimulation', function(simulationQueue, appState, a
}
//TODO(robnagler) should be part of simulationStatus
frameCache.setFrameCount(0);
+ srCache.clearFrames(appState.models.simulation.simulationId);
setSimulationStatus({state: 'pending'});
state.simulationQueueItem = simulationQueue.addPersistentItem(
state.model,