From 9561142c43121f4babd342574a5af5e0a2efb4b7 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 25 Oct 2016 14:58:27 -0700 Subject: [PATCH] improvements --- .../tests/performance/domain-check.js | 223 ++++++++++++++---- 1 file changed, 177 insertions(+), 46 deletions(-) diff --git a/scripts/developer/tests/performance/domain-check.js b/scripts/developer/tests/performance/domain-check.js index f085c3f685..13b46c0c58 100644 --- a/scripts/developer/tests/performance/domain-check.js +++ b/scripts/developer/tests/performance/domain-check.js @@ -17,37 +17,157 @@ var MINIMUM_DESKTOP_FRAMERATE = 57; // frames per second var MINIMUM_HMD_FRAMERATE = 86; var EXPECTED_DESKTOP_FRAMERATE = 60; var EXPECTED_HMD_FRAMERATE = 90; -var MAXIMUM_LOAD_TIME = 60; // seconds -var MINIMUM_AVATARS = 25; // FIXME: not implemented yet. Requires agent scripts. Idea is to have them organize themselves to the right number. +var NOMINAL_LOAD_TIME = 30; // seconds +var MAXIMUM_LOAD_TIME = NOMINAL_LOAD_TIME * 2; +var MINIMUM_AVATARS = 25; // changeable by prompt -var version = 2; +var DENSITY = 0.3; // square meters per person. Some say 10 sq ft is arm's length (0.9m^2), 4.5 is crowd (0.4m^2), 2.5 is mosh pit (0.2m^2). +var SOUND_DATA = {url: "http://howard-stearns.github.io/models/sounds/piano1.wav"}; +var AVATARS_CHATTERING_AT_ONCE = 4; // How many of the agents should we request to play SOUND at once. +var NEXT_SOUND_SPREAD = 500; // millisecond range of how long to wait after one sound finishes, before playing the next +var ANIMATION_DATA = { + "url": "http://howard-stearns.github.io/models/resources/avatar/animations/idle.fbx", + // "url": "http://howard-stearns.github.io/models/resources/avatar/animations/walk_fwd.fbx", // alternative example + "startFrame": 0.0, + "endFrame": 300.0, + "timeScale": 1.0, + "loopFlag": true +}; + +var version = 11; function debug() { print.apply(null, [].concat.apply(['hrs fixme', version], [].map.call(arguments, JSON.stringify))); } +var cachePlaces = ['localhost', 'welcome'].map(canonicalizePlacename); // For now, list the lighter weight one first. +var defaultPlace = location.hostname; +var prompt = "domain-check.js version " + version + "\n\nWhat place should we enter?"; +debug(cachePlaces, defaultPlace, prompt); +var entryPlace = Window.prompt(prompt, defaultPlace); +var runTribbles = Window.confirm("Run tribbles?\n\n\ +At most, only one participant should say yes."); +MINIMUM_AVATARS = parseInt(Window.prompt("Total avatars (including yourself and any already present)?", MINIMUM_AVATARS.toString()) || "0"); +AVATARS_CHATTERING_AT_ONCE = MINIMUM_AVATARS ? parseInt(Window.prompt("Number making sound?", Math.min(MINIMUM_AVATARS - 1, AVATARS_CHATTERING_AT_ONCE).toString()) || "0") : 0; + +function canonicalizePlacename(name) { + var prefix = 'dev-'; + name = name.toLowerCase(); + if (name.indexOf(prefix) === 0) { + name = name.slice(prefix.length); + } + return name; +} +function placesMatch(a, b) { // handling case and 'dev-' variations + return canonicalizePlacename(a) === canonicalizePlacename(b); +} function isNowIn(place) { // true if currently in specified place - return location.hostname.toLowerCase() === place.toLowerCase(); + placesMatch(location.hostname, place); } -var cachePlaces = ['dev-Welcome', 'localhost']; // For now, list the lighter weight one first. -var isInCachePlace = cachePlaces.some(isNowIn); -var defaultPlace = isInCachePlace ? 'dev-Playa' : location.hostname; -var prompt = "domain-check.js version " + version + "\n\nWhat place should we enter?"; -debug(cachePlaces, isInCachePlace, defaultPlace, prompt); -var entryPlace = Window.prompt(prompt, defaultPlace); +function go(place) { // handle (dev-)welcome in the appropriate version-specific way + debug('go', place); + if (placesMatch(place, 'welcome')) { + location.goToEntry(); + } else { + location.handleLookupString(place); + } +} + +var spread = Math.sqrt(MINIMUM_AVATARS * DENSITY); // meters +var turnSpread = 90; // How many degrees should turn from front range over. + +function coord() { return (Math.random() * spread) - (spread / 2); } // randomly distribute a coordinate zero += spread/2. +function contains(array, item) { return array.indexOf(item) >= 0; } +function without(array, itemsToRemove) { return array.filter(function (item) { return !contains(itemsToRemove, item); }); } +function nextAfter(array, id) { // Wrapping next element in array after id. + var index = array.indexOf(id) + 1; + return array[(index >= array.length) ? 0 : index]; +} + +var summonedAgents = []; +var chattering = []; +var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; +function messageSend(message) { + Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); +} +function messageHandler(channel, messageString, senderID) { + if (channel !== MESSAGE_CHANNEL) { + return; + } + debug('message', channel, messageString, senderID); + if (MyAvatar.sessionUUID === senderID) { // ignore my own + return; + } + var message = {}, avatarIdentifiers; + try { + message = JSON.parse(messageString); + } catch (e) { + print(e); + } + switch (message.key) { + case "hello": + // There can be avatars we've summoned that do not yet appear in the AvatarList. + avatarIdentifiers = without(AvatarList.getAvatarIdentifiers(), summonedAgents); + debug('present', avatarIdentifiers, summonedAgents); + if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS ) { + var chatter = chattering.length < AVATARS_CHATTERING_AT_ONCE; + if (chatter) { + chattering.push(senderID); + } + summonedAgents.push(senderID); + messageSend({ + key: 'SUMMON', + rcpt: senderID, + position: Vec3.sum(MyAvatar.position, {x: coord(), y: 0, z: coord()}), + orientation: Quat.fromPitchYawRollDegrees(0, Quat.safeEulerAngles(MyAvatar.orientation).y + (turnSpread * (Math.random() - 0.5)), 0), + soundData: chatter && SOUND_DATA, + skeletonModelURL: "http://howard-stearns.github.io/models/resources/meshes/defaultAvatar_full.fst", + animationData: ANIMATION_DATA + }); + } + break; + case "finishedSound": // Give someone else a chance. + chattering = without(chattering, [senderID]); + Script.setTimeout(function () { + messageSend({ + key: 'SUMMON', + rcpt: nextAfter(without(summonedAgents, chattering), senderID), + soundData: SOUND_DATA + }); + }, Math.random() * NEXT_SOUND_SPREAD); + break; + case "HELO": + Window.alert("Someone else is summoning avatars."); + break; + default: + print("crowd-agent received unrecognized message:", messageString); + } +} +Messages.subscribe(MESSAGE_CHANNEL); +Messages.messageReceived.connect(messageHandler); +Script.scriptEnding.connect(function () { + debug('stopping agents', summonedAgents); + summonedAgents.forEach(function (id) { messageSend({key: 'STOP', rcpt: id}); }); + debug('agents stopped'); + Script.setTimeout(function () { + Messages.messageReceived.disconnect(messageHandler); + Messages.unsubscribe(MESSAGE_CHANNEL); + debug('unsubscribed'); + }, 500); +}); var fail = false, results = ""; -function addResult(label, actual, minimum, maximum) { +function addResult(label, actual, nominal, minimum, maximum) { if ((minimum !== undefined) && (actual < minimum)) { - fail = true; + fail = ' FAILED: ' + label + ' below ' + minimum; } if ((maximum !== undefined) && (actual > maximum)) { - fail = true; + fail = ' FAILED: ' + label + ' above ' + maximum; } - results += "\n" + label + ": " + actual + " (" + ((100 * actual) / (maximum || minimum)).toFixed(0) + "%)"; + results += "\n" + label + ": " + actual.toFixed(0) + " (" + ((100 * actual) / nominal).toFixed(0) + "%)"; } function giveReport() { - Window.alert(entryPlace + (fail ? " FAILED" : " OK") + "\n" + results); + Window.alert(entryPlace + (fail || " OK") + "\n" + results + "\nwith " + summonedAgents.length + " avatars added,\nand " + AVATARS_CHATTERING_AT_ONCE + " making noise."); } // Tests are performed domain-wide, at full LOD @@ -122,9 +242,8 @@ function doLoad(place, continuationWithLoadTime) { // Go to place and call conti } }; - debug('go', place); location.hostChanged.connect(waitForLoad); - location.handleLookupString(place); + go(place); } var config = Render.getConfig("Stats"); @@ -133,48 +252,59 @@ function doRender(continuation) { function onNewStats() { // Accumulates frames on signal during load test frames++; } + if (MINIMUM_AVATARS) { + messageSend({key: 'HELO'}); // Ask agents to report in now. + } + config.newStats.connect(onNewStats); startTwirl(720, 1, 15, 0.08, function () { var end = Date.now(); config.newStats.disconnect(onNewStats); addResult('frame rate', 1000 * frames / (end - start), - HMD.active ? MINIMUM_HMD_FRAMERATE : MINIMUM_DESKTOP_FRAMERATE, - HMD.active ? EXPECTED_HMD_FRAMERATE : EXPECTED_DESKTOP_FRAMERATE); + HMD.active ? EXPECTED_HMD_FRAMERATE : EXPECTED_DESKTOP_FRAMERATE, + HMD.active ? MINIMUM_HMD_FRAMERATE : MINIMUM_DESKTOP_FRAMERATE); + var total = AvatarList.getAvatarIdentifiers().length; + if (MINIMUM_AVATARS && !fail) { + if (0 === summonedAgents.length) { + fail = "FAIL: No agents reported.\n\Please run " + MINIMUM_AVATARS + " instances of\n\ +http://cdn.highfidelity.com/davidkelly/production/scripts/tests/performance/crowd-agent.js\n\ +on your domain server."; + } else if (total < MINIMUM_AVATARS) { + fail = "FAIL: Only " + summonedAgents.length + " of the expected " + (MINIMUM_AVATARS - total) + " agents reported in."; + } + } continuation(); }); } var TELEPORT_PAUSE = 500; -function maybePrepareCache(continuation) { - var prepareCache = Window.confirm("Prepare cache?\n\n\ -Should we start with all and only those items cached that are encountered when visiting:\n" + cachePlaces.join(', ') + "\n\ -If 'yes', cache will be cleared and we will visit these two, with a turn in each, and wait for everything to be loaded.\n\ -You would want to say 'no' (and make other preparations) if you were testing these places."); - - if (prepareCache) { - function loadNext() { - var place = cachePlaces.shift(); - doLoad(place, function (prepTime) { - debug(place, 'ready', prepTime); - if (cachePlaces.length) { - loadNext(); - } else { - continuation(); - } - }); - } - location.handleLookupString(cachePlaces[cachePlaces.length - 1]); +function prepareCache(continuation) { + function loadNext() { + var place = cachePlaces.shift(); + doLoad(place, function (prepTime) { + debug(place, 'ready', prepTime); + if (cachePlaces.length) { + loadNext(); + } else { + continuation(); + } + }); + } + // remove entryPlace target from cachePlaces + var targetInCache = cachePlaces.indexOf(canonicalizePlacename(entryPlace)); + if (targetInCache !== -1) { + cachePlaces.splice(targetInCache, 1); + } + debug('cachePlaces', cachePlaces) + go(cachePlaces[1] || entryPlace); // Not quite right for entryPlace case (allows some qt pre-caching), but close enough. + Script.setTimeout(function () { Menu.triggerOption("Reload Content (Clears all caches)"); Script.setTimeout(loadNext, TELEPORT_PAUSE); - } else { - location.handleLookupString(isNowIn(cachePlaces[0]) ? cachePlaces[1] : cachePlaces[0]); - Script.setTimeout(continuation, TELEPORT_PAUSE); - } + }, TELEPORT_PAUSE); } function maybeRunTribbles(continuation) { - if (Window.confirm("Run tribbles?\n\n\ -At most, only one participant should say yes.")) { + if (runTribbles) { Script.load('http://cdn.highfidelity.com/davidkelly/production/scripts/tests/performance/tribbles.js'); Script.setTimeout(continuation, 3000); } else { @@ -186,10 +316,11 @@ if (!entryPlace) { Window.alert("domain-check.js cancelled"); Script.stop(); } else { - maybePrepareCache(function (prepTime) { + prepareCache(function (prepTime) { debug('cache ready', prepTime); doLoad(entryPlace, function (loadTime) { - addResult("load time", loadTime, undefined, MAXIMUM_LOAD_TIME); + addResult("load time", loadTime, NOMINAL_LOAD_TIME, undefined, MAXIMUM_LOAD_TIME); + LODManager.setAutomaticLODAdjust(initialLodIsAutomatic); // after loading, restore lod. maybeRunTribbles(function () { doRender(function () { giveReport();