Permalink
Browse files

Introduce `/lvp profile` for Management members

This enables them to create a CPU profile whenever they want.
  • Loading branch information...
RussellLVP committed Oct 27, 2016
1 parent 1439323 commit e3f2a6f70af3dd8199f1107b3d96d4ca82355681
View
@@ -214,6 +214,8 @@
"LVP_ANNOUNCE_CMD_REMOVED_EXCEPTION": "%s (Id: %d) has revoked %s ability to use /%s.",
"LVP_ANNOUNCE_CMD_REVOKED": "%s has removed the /%s command from players again.",
"LVP_ANNOUNCE_FEATURE_RELOADED": "%s (Id: %d) has reloaded the '%s' feature.",
"LVP_ANNOUNCE_PROFILE_FINISHED": "The profile initiated by %s has been concluded.",
"LVP_ANNOUNCE_PROFILE_START": "%s (Id: %d) has started profiling the JavaScript code for %dms.",
"LVP_ANNOUNCE_SETTING_TOGGLED": "%s (Id: %d) has %s the '%s' setting.",
"LVP_ANNOUNCE_SETTING_UPDATED_NUM": "%s (Id: %d) has updated the '%s' setting to '%d'.",
"LVP_CMD_EXCEPTION_GRANTED": "{ADFF2F}*** %s (Id: %d) has given you an exception to use /%s.",
@@ -230,6 +232,10 @@
"LVP_PLAYGROUND_OPTIONS": "@usage /lvp %s [on/off]",
"LVP_PLAYGROUND_OPTION_STATUS": "The {FF8282}%s{FFFFFF} option is {FF8282}%s{FFFFFF}. Type {FF8282}/lvp %s [on/off]{FFFFFF} to change this.",
"LVP_PLAYGROUND_OPTION_NO_CHANGE": "@error The {FF8282}%s{FFFFFF} feature has already been %s.",
"LVP_PROFILE_FINISHED": "@success The CPU profile that you requested has been written to %s.",
"LVP_PROFILE_INVALID_RANGE": "@error CPU profiles must be between %d and %d milliseconds in duration.",
"LVP_PROFILE_ONGOING": "@error A CPU profile is already in progress of being captured.",
"LVP_PROFILE_STARTED": "@success A CPU profile has been started for %d milliseconds.",
"LVP_RELOAD_NOT_ELIGIBLE": "@error The {FF8282}%s{FFFFFF} feature is not eligible for live reloading.",
"LVP_RELOAD_RELOADED": "@success The %s feature has been reloaded.",
"LVP_SETTING_INVALID_NUMBER": "Whuh? {FFA500}%s{A9C4E4} is not a number, you silly billy!",
@@ -9,6 +9,7 @@ server and the commands offered on the server.
- **/lvp access**: Changes access rights for one of the commands in this feature.
- **/lvp party [on/off]**: Toggles the Party Mode on or off. Only available to Management.
- **/lvp profile [milliseconds]**: Enables Management to create a CPU profile of the gamemode.
- **/lvp settings**: Enables Management to change various run-time settings on the server.
## Commands
@@ -10,6 +10,9 @@ const PlaygroundAccessTracker = require('features/playground/playground_access_t
const Question = require('components/dialogs/question.js');
const Setting = require('features/settings/setting.js');
// Directory in which the CPU profiles will be stored.
const ProfileDirectory = 'profiles';
// Utility function to capitalize the first letter of a |string|.
function capitalizeFirstLetter(string) {
return string[0].toUpperCase() + string.slice(1);
@@ -63,6 +66,15 @@ class PlaygroundCommands {
// -----------------------------------------------------------------------------------------
this.profiling_ = false;
// Functor that will actually activate a CPU profile. Has to be overridden by tests in order
// to avoid starting a CPU profile by accident.
this.captureProfileFn_ = (milliseconds, filename) =>
captureProfile(milliseconds, filename);
// -----------------------------------------------------------------------------------------
// The `/lvp` command offers administrators and higher a number of functions to manage the
// server, the available commands and availability of a number of smaller features.
server.commandManager.buildCommand('lvp')
@@ -75,6 +87,10 @@ class PlaygroundCommands {
{ name: 'enabled', type: CommandBuilder.WORD_PARAMETER, optional: true }
])
.build(PlaygroundCommands.prototype.onPlaygroundOptionCommand.bind(this, 'party'))
.sub('profile')
.restrict(Player.LEVEL_MANAGEMENT)
.parameters([ { name: 'milliseconds', type: CommandBuilder.NUMBER_PARAMETER } ])
.build(PlaygroundCommands.prototype.onPlaygroundProfileCommand.bind(this))
.sub('reload')
.restrict(Player.LEVEL_MANAGEMENT)
.parameters([ { name: 'feature', type: CommandBuilder.WORD_PARAMETER } ])
@@ -317,6 +333,58 @@ class PlaygroundCommands {
Message.LVP_ANNOUNCE_ADMIN_NOTICE, player.name, player.id, updatedStatusText, option);
}
// Enables Management members to capture a CPU profile of the gamemode for a given number of
// |profileDurationMs|. The filename of the profile will be automatically decided.
onPlaygroundProfileCommand(player, profileDurationMs) {
const MinimumDurationMs = 100;
const MaximumDurationMs = 180000;
if (profileDurationMs < MinimumDurationMs || profileDurationMs > MaximumDurationMs) {
player.sendMessage(
Message.LVP_PROFILE_INVALID_RANGE, MinimumDurationMs, MaximumDurationMs);
return;
}
if (this.profiling_) {
player.sendMessage(Message.LVP_PROFILE_ONGOING);
return;
}
function zeroPad(value) {
return ('0' + value).substr(-2);
}
const date = new Date();
// Compile the filename for the trace based on the current time on the server.
const filename =
'profile_' + date.getFullYear() + '-' + zeroPad(date.getMonth() + 1) + '-' +
zeroPad(date.getDate()) + '_' + zeroPad(date.getHours()) + '-' +
zeroPad(date.getMinutes()) + '-' + zeroPad(date.getSeconds()) + '.log';
// Start an asynchronous function that will report the profile as having finished.
(async() => {
await milliseconds(profileDurationMs);
this.announce_().announceToAdministrators(
Message.LVP_ANNOUNCE_PROFILE_FINISHED, player.name, filename);
if (player.isConnected())
player.sendMessage(Message.LVP_PROFILE_FINISHED, filename);
this.profiling_ = false;
})();
// Capture the profile through a functor that can be overridden for testing purposes.
this.captureProfileFn_(profileDurationMs, ProfileDirectory + '/' + filename);
this.profiling_ = true;
this.announce_().announceToAdministrators(
Message.LVP_ANNOUNCE_PROFILE_START, player.name, player.id, profileDurationMs);
player.sendMessage(Message.LVP_PROFILE_STARTED, profileDurationMs);
}
// Facilitates the developer's ability to reload features without having to restart the server.
// There are strict requirements a feature has to meet in regards to dependencies in order for
// it to be live reloadable.
@@ -499,7 +567,7 @@ class PlaygroundCommands {
options.push('access');
if (player.isManagement())
options.push('party', 'reload', 'settings');
options.push('party', 'profile', 'reload', 'settings');
player.sendMessage(Message.LVP_PLAYGROUND_HEADER);
if (!options.length)
@@ -154,6 +154,76 @@ describe('PlaygroundCommands', (it, beforeEach, afterEach) => {
assert.equal(access.getDefaultCommandLevel(COMMAND_NAME), Player.LEVEL_ADMINISTRATOR);
});
it('should be able to capture profiles with a given duration', async(assert) => {
gunther.level = Player.LEVEL_MANAGEMENT;
let captureMilliseconds = null;
let captureFilename = null;
commands.captureProfileFn_ = (milliseconds, filename) => {
captureMilliseconds = milliseconds;
captureFilename = filename;
};
// (1) It should validate the valid range of the profile duration.
{
assert.isTrue(await gunther.issueCommand('/lvp profile 42'));
assert.isTrue(await gunther.issueCommand('/lvp profile 240000'));
const expected = Message.format(Message.LVP_PROFILE_INVALID_RANGE, 100, 180000);
assert.equal(gunther.messages.length, 2);
assert.equal(gunther.messages[0], expected);
assert.equal(gunther.messages[1], expected);
gunther.clearMessages();
}
let filename = null;
// (2) It should be able to start a profile.
{
assert.isTrue(await gunther.issueCommand('/lvp profile 30000'));
assert.equal(gunther.messages.length, 2);
assert.equal(gunther.messages[1], Message.format(Message.LVP_PROFILE_STARTED, 30000));
assert.isTrue(gunther.messages[0].includes(
Message.format(Message.LVP_ANNOUNCE_PROFILE_START, gunther.name,
gunther.id, 30000)));
assert.isNotNull(captureMilliseconds);
assert.equal(captureMilliseconds, 30000);
assert.isNotNull(captureFilename);
[, filename] = captureFilename.split('/'); // basename(captureFilename)
gunther.clearMessages();
}
// (3) It should not allow multiple profiles to run in parallel.
{
assert.isTrue(await gunther.issueCommand('/lvp profile 30000'));
assert.equal(gunther.messages.length, 1);
assert.equal(gunther.messages[0], Message.LVP_PROFILE_ONGOING);
gunther.clearMessages();
}
await server.clock.advance(30000); // time of the profile
// (4) It should inform the issuer and administrators when the profile has finished.
{
assert.equal(gunther.messages.length, 2);
assert.isTrue(gunther.messages[0].includes(
Message.format(Message.LVP_ANNOUNCE_PROFILE_FINISHED, gunther.name,
filename)));
assert.equal(
gunther.messages[1], Message.format(Message.LVP_PROFILE_FINISHED, filename));
}
});
it('should be able to change boolean settings', async(assert) => {
const settings = server.featureManager.loadFeature('settings');

0 comments on commit e3f2a6f

Please sign in to comment.