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,