overte-thingvellir/scripts/developer/tests/performance/domain-check.js

345 lines
14 KiB
JavaScript

"use strict";
/*jslint vars: true, plusplus: true*/
/*globals Script, MyAvatar, Quat, Vec3, Render, ScriptDiscoveryService, Window, LODManager, Entities, Messages, AvatarList, Menu, Stats, HMD, location, 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
//
// Confirms that the specified domain is operating within specified constraints.
var MINIMUM_DESKTOP_FRAMERATE = 57; // frames per second
var MINIMUM_HMD_FRAMERATE = 86;
var EXPECTED_DESKTOP_FRAMERATE = 60;
var EXPECTED_HMD_FRAMERATE = 90;
var NOMINAL_LOAD_TIME = 30; // seconds
var MAXIMUM_LOAD_TIME = NOMINAL_LOAD_TIME * 2;
var MINIMUM_AVATARS = 25; // changeable by prompt
// If we add or remove things too quickly, we get problems (e.g., audio, fogbugz 2095).
// For now, spread them out this timing apart.
var SPREAD_TIME_MS = 500;
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: Script.getExternalPath(Script.ExternalPaths.HF_Content, "/howard/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": Script.getExternalPath(Script.ExternalPaths.HF_Content, "/howard/resources/avatar/animations/idle.fbx"),
// "url": Script.getExternalPath(Script.ExternalPaths.HF_Content, "/howard/resources/avatar/animations/walk_fwd.fbx"), // alternative example
"startFrame": 0.0,
"endFrame": 300.0,
"timeScale": 1.0,
"loopFlag": true
};
var version = 4;
function debug() {
print.apply(null, [].concat.apply(['hrs fixme', version], [].map.call(arguments, JSON.stringify)));
}
function canonicalizePlacename(name) {
var prefix = 'dev-';
name = name.toLowerCase();
if (name.indexOf(prefix) === 0) {
name = name.slice(prefix.length);
}
return name;
}
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", 10);
AVATARS_CHATTERING_AT_ONCE = MINIMUM_AVATARS ? parseInt(Window.prompt("Number making sound?", Math.min(MINIMUM_AVATARS - 1, AVATARS_CHATTERING_AT_ONCE).toString()) || "0", 10) : 0;
function placesMatch(a, b) { // handling case and 'dev-' variations
return canonicalizePlacename(a) === canonicalizePlacename(b);
}
function isNowIn(place) { // true if currently in specified place
placesMatch(location.hostname, place);
}
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 accumulatedDelay = 0;
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) {
}
switch (message.key) {
case "hello":
Script.setTimeout(function () {
// 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,
listen: true,
skeletonModelURL: Script.getExternalPath(Script.ExternalPaths.HF_Content, "/howard/resources/meshes/defaultAvatar_full.fst"),
animationData: ANIMATION_DATA
});
}
}, accumulatedDelay);
accumulatedDelay += SPREAD_TIME_MS; // assume we'll get all the hello respsponses more or less together.
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");
}
}
Messages.subscribe(MESSAGE_CHANNEL);
Messages.messageReceived.connect(messageHandler);
Script.scriptEnding.connect(function () {
debug('stopping agents', summonedAgents);
Messages.messageReceived.disconnect(messageHandler); // don't respond to any messages during shutdown
accumulatedDelay = 0;
summonedAgents.forEach(function (id) {
messageSend({key: 'STOP', rcpt: id, delay: accumulatedDelay});
accumulatedDelay += SPREAD_TIME_MS;
});
debug('agents stopped');
Messages.unsubscribe(MESSAGE_CHANNEL);
debug('unsubscribed');
});
var fail = false, results = "";
function addResult(label, actual, nominal, minimum, maximum) {
if ((minimum !== undefined) && (actual < minimum)) {
fail = ' FAILED: ' + label + ' below ' + minimum;
}
if ((maximum !== undefined) && (actual > maximum)) {
fail = ' FAILED: ' + label + ' above ' + maximum;
}
results += "\n" + label + ": " + actual.toFixed(0) + " (" + ((100 * actual) / nominal).toFixed(0) + "%)";
}
function giveReport() {
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
var initialLodIsAutomatic = LODManager.getAutomaticLODAdjust();
var LOD = 32768 * 400;
LODManager.setAutomaticLODAdjust(false);
LODManager.setOctreeSizeScale(LOD);
Script.scriptEnding.connect(function () { LODManager.setAutomaticLODAdjust(initialLodIsAutomatic); });
function startTwirl(targetRotation, degreesPerUpdate, interval, strafeDistance, optionalCallback) {
var initialRotation = Quat.safeEulerAngles(MyAvatar.orientation).y;
var accumulatedRotation = 0;
function tick() {
MyAvatar.orientation = Quat.fromPitchYawRollDegrees(0, accumulatedRotation + initialRotation, 0);
if (strafeDistance) {
MyAvatar.position = Vec3.sum(MyAvatar.position, Vec3.multiply(strafeDistance, Quat.getRight(MyAvatar.orientation)));
}
accumulatedRotation += degreesPerUpdate;
if (accumulatedRotation >= targetRotation) {
return optionalCallback && optionalCallback();
}
Script.setTimeout(tick, interval);
}
tick();
}
function doLoad(place, continuationWithLoadTime) { // Go to place and call continuationWithLoadTime(loadTimeInSeconds)
var start = Date.now(), timeout, onDownloadUpdate, finishedTwirl = false, loadTime;
// There are two ways to learn of changes: connect to change signals, or poll.
// Until we get reliable results, we'll poll.
var POLL_INTERVAL = 500, poll;
function setHandlers() {
//Stats.downloadsPendingChanged.connect(onDownloadUpdate); downloadsChanged, and physics...
poll = Script.setInterval(onDownloadUpdate, POLL_INTERVAL);
}
function clearHandlers() {
debug('clearHandlers');
//Stats.downloadsPendingChanged.disconnect(onDownloadUpdate); downloadsChanged, and physics..
Script.clearInterval(poll);
}
function waitForLoad(flag) {
debug('entry', place, 'initial downloads/pending', Stats.downloads, Stats.downloadsPending);
location.hostChanged.disconnect(waitForLoad);
timeout = Script.setTimeout(function () {
debug('downloads timeout', Date());
clearHandlers();
Window.alert("Timeout during " + place + " load. FAILED");
Script.stop();
}, MAXIMUM_LOAD_TIME * 1000);
startTwirl(360, 6, 90, null, function () {
finishedTwirl = true;
if (loadTime) {
continuationWithLoadTime(loadTime);
}
});
setHandlers();
}
function isLoading() {
// FIXME: We should also confirm that textures have loaded.
return Stats.downloads || Stats.downloadsPending || !Window.isPhysicsEnabled();
}
onDownloadUpdate = function onDownloadUpdate() {
debug('update downloads/pending', Stats.downloads, Stats.downloadsPending);
if (isLoading()) {
return;
}
Script.clearTimeout(timeout);
clearHandlers();
loadTime = (Date.now() - start) / 1000;
if (finishedTwirl) {
continuationWithLoadTime(loadTime);
}
};
location.hostChanged.connect(waitForLoad);
go(place);
}
var config = Render.getConfig("Stats");
function doRender(continuation) {
var start = Date.now(), frames = 0;
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, 20, 0.08, function () {
var end = Date.now();
config.newStats.disconnect(onNewStats);
addResult('frame rate', 1000 * frames / (end - start),
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.\nPlease run " + MINIMUM_AVATARS + " instances of\n\
https://cdn-1.vircadia.com/eu-c-1/howard/scripts/tests/performance/crowd-agent.js?v=3\n\
on your domain server.";
} else if (total < MINIMUM_AVATARS) {
fail = "FAIL: Only " + summonedAgents.length + " agents reported. Now missing " + (MINIMUM_AVATARS - total) + " avatars, total.";
}
}
continuation();
});
}
var TELEPORT_PAUSE = 500;
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);
}, TELEPORT_PAUSE);
}
function maybeRunTribbles(continuation) {
if (runTribbles) {
Script.load('http://cdn.highfidelity.com/davidkelly/production/scripts/tests/performance/tribbles.js');
Script.setTimeout(continuation, 3000);
} else {
continuation();
}
}
if (!entryPlace) {
Window.alert("domain-check.js cancelled");
Script.stop();
} else {
prepareCache(function (prepTime) {
debug('cache ready', prepTime);
doLoad(entryPlace, function (loadTime) {
addResult("load time", loadTime, NOMINAL_LOAD_TIME, undefined, MAXIMUM_LOAD_TIME);
LODManager.setAutomaticLODAdjust(initialLodIsAutomatic); // after loading, restore lod.
maybeRunTribbles(function () {
doRender(function () {
giveReport();
Script.stop();
});
});
});
});
}
Script.scriptEnding.connect(function () { print("domain-check completed"); });