"use strict"; /*jslint vars: true, plusplus: true*/ /*globals Script, MyAvatar, Quat, Render, ScriptDiscoveryService, Window, LODManager, Entities, print*/ // // loadedMachine.js // scripts/developer/tests/ // // Created by Howard Stearns on 6/6/16. // Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // // Characterises the performance of heavily loaded or struggling machines. // There is no point in running this on a machine that producing the target 60 or 90 hz rendering rate. // // The script examines several source of load, including each running script and a couple of Entity types. // It turns all of these off, as well as LOD management, and twirls in place to get a baseline render rate. // Then it turns each load on, one at a time, and records a new render rate. // At the end, it reports the ordered results (in a popup), restores the original loads and LOD management, and quits. // // Small performance changes are not meaningful. // If you think you find something that is significant, you should probably run again to make sure that it isn't random variation. // You can also compare (e.g., the baseline) for different conditions such as domain, version, server settings, etc. // This does not profile scripts as they are used -- it merely measures the effect of having the script loaded and running quietly. var NUMBER_OF_TWIRLS_PER_LOAD = 10; var MILLISECONDS_PER_SECOND = 1000; var WAIT_TIME_MILLISECONDS = MILLISECONDS_PER_SECOND; var accumulatedRotation = 0; var start; var frames = 0; var config = Render.getConfig("Stats"); var thisPath = Script.resolvePath(''); var scriptData = ScriptDiscoveryService.getRunning(); var scripts = scriptData.filter(function (datum) { return datum.name !== 'defaultScripts.js'; }).map(function (script) { return script.path; }); // If defaultScripts.js is running, we leave it running, because restarting it safely is a mess. var otherScripts = scripts.filter(function (path) { return path !== thisPath; }); var numberLeftRunning = scriptData.length - otherScripts.length; print('initially running', otherScripts.length, 'scripts. Leaving', numberLeftRunning, 'and stopping otherScripts'); var typedEntities = {Light: [], ParticleEffect: []}; var interestingTypes = Object.keys(typedEntities); var propertiedEntities = {dynamic: []}; var interestingProperties = Object.keys(propertiedEntities); var loads = ['ignore', 'baseline'].concat(otherScripts, interestingTypes, interestingProperties); var loadIndex = 0, nLoads = loads.length, load; var results = []; var initialLodIsAutomatic = LODManager.getAutomaticLODAdjust(); var SEARCH_RADIUS = 17000; var DEFAULT_LOD = 32768 * 400; LODManager.setAutomaticLODAdjust(false); LODManager.setOctreeSizeScale(DEFAULT_LOD); // Fill the typedEnties with the entityIDs that are already visible. It would be nice if this were more efficient. var allEntities = Entities.findEntities(MyAvatar.position, SEARCH_RADIUS); print('Searching', allEntities.length, 'entities for', interestingTypes, 'and', interestingProperties); var propertiesToGet = ['type', 'visible'].concat(interestingProperties); allEntities.forEach(function (entityID) { var properties = Entities.getEntityProperties(entityID, propertiesToGet); if (properties.visible) { if (interestingTypes.indexOf(properties.type) >= 0) { typedEntities[properties.type].push(entityID); } else { interestingProperties.forEach(function (property) { if (entityID && properties[property]) { propertiedEntities[property].push(entityID); entityID = false; // Put in only one bin } }); } } }); allEntities = undefined; // free them interestingTypes.forEach(function (type) { print('There are', typedEntities[type].length, type, 'entities.'); }); interestingProperties.forEach(function (property) { print('There are', propertiedEntities[property].length, property, 'entities.'); }); function toggleVisibility(type, on) { typedEntities[type].forEach(function (entityID) { Entities.editEntity(entityID, {visible: on}); }); } function toggleProperty(property, on) { propertiedEntities[property].forEach(function (entityID) { var properties = {}; properties[property] = on; Entities.editEntity(entityID, properties); }); } function restoreOneTest(load) { print('restore', load); switch (load) { case 'baseline': case 'ignore': // ignore is used to do a twirl to make sure everything is loaded. break; case 'Light': case 'ParticleEffect': toggleVisibility(load, true); break; case 'dynamic': toggleProperty(load, 1); break; default: Script.load(load); } } function undoOneTest(load) { print('stop', load); switch (load) { case 'baseline': case 'ignore': break; case 'Light': case 'ParticleEffect': toggleVisibility(load, false); break; case 'dynamic': toggleProperty(load, 0); break; default: ScriptDiscoveryService.stopScript(load); } } loads.forEach(undoOneTest); function finalReport() { var baseline = results[0]; results.forEach(function (result) { result.ratio = (baseline.fps / result.fps); }); results.sort(function (a, b) { return b.ratio - a.ratio; }); var report = 'Performance Report:\nBaseline: ' + baseline.fps.toFixed(1) + ' fps.'; results.forEach(function (result) { report += '\n' + result.ratio.toFixed(2) + ' (' + result.fps.toFixed(1) + ' fps over ' + result.elapsed + ' seconds) for ' + result.name; }); Window.alert(report); LODManager.setAutomaticLODAdjust(initialLodIsAutomatic); loads.forEach(restoreOneTest); Script.stop(); } function onNewStats() { // Accumulates frames on signal during load test frames++; } var DEGREES_PER_FULL_TWIRL = 360; var DEGREES_PER_UPDATE = 6; function onUpdate() { // Spins on update signal during load test MyAvatar.orientation = Quat.fromPitchYawRollDegrees(0, accumulatedRotation, 0); accumulatedRotation += DEGREES_PER_UPDATE; if (accumulatedRotation >= (DEGREES_PER_FULL_TWIRL * NUMBER_OF_TWIRLS_PER_LOAD)) { tearDown(); } } function startTest() { print('start', load); accumulatedRotation = frames = 0; start = Date.now(); Script.update.connect(onUpdate); config.newStats.connect(onNewStats); } function setup() { if (loadIndex >= nLoads) { return finalReport(); } load = loads[loadIndex]; restoreOneTest(load); Script.setTimeout(startTest, WAIT_TIME_MILLISECONDS); } function tearDown() { var now = Date.now(); var elapsed = (now - start) / MILLISECONDS_PER_SECOND; Script.update.disconnect(onUpdate); config.newStats.disconnect(onNewStats); if (load !== 'ignore') { results.push({name: load, fps: frames / elapsed, elapsed: elapsed}); } undoOneTest(load); loadIndex++; setup(); } function waitUntilStopped() { // Wait until all the scripts have stopped var count = ScriptDiscoveryService.getRunning().length; if (count === numberLeftRunning) { return setup(); } print('Still', count, 'scripts running'); Script.setTimeout(waitUntilStopped, WAIT_TIME_MILLISECONDS); } waitUntilStopped();