mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 13:21:05 +00:00
Bug 1458466 - Implement Console.timeLog(optional DOMString label = "default"), r=bgrins
This commit is contained in:
parent
334f27e152
commit
325b9045d9
@ -108,6 +108,7 @@ method console.group
|
||||
method console.groupCollapsed
|
||||
method console.groupEnd
|
||||
method console.time
|
||||
method console.timeLog
|
||||
method console.timeEnd
|
||||
method console.exception
|
||||
method console.timeStamp
|
||||
|
@ -94,8 +94,8 @@ public:
|
||||
, mTimeStamp(JS_Now() / PR_USEC_PER_MSEC)
|
||||
, mStartTimerValue(0)
|
||||
, mStartTimerStatus(Console::eTimerUnknown)
|
||||
, mStopTimerDuration(0)
|
||||
, mStopTimerStatus(Console::eTimerUnknown)
|
||||
, mLogTimerDuration(0)
|
||||
, mLogTimerStatus(Console::eTimerUnknown)
|
||||
, mCountValue(MAX_PAGE_COUNTERS)
|
||||
, mIDType(eUnknown)
|
||||
, mOuterIDNumber(0)
|
||||
@ -221,14 +221,14 @@ public:
|
||||
Console::TimerStatus mStartTimerStatus;
|
||||
|
||||
// These values are set in the owning thread and they contain the duration,
|
||||
// the name and the status of the StopTimer method. If status is false,
|
||||
// the name and the status of the LogTimer method. If status is false,
|
||||
// something went wrong. They will be set on the owning thread and never
|
||||
// touched again on that thread. They will be used in order to create a
|
||||
// ConsoleTimerEnd dictionary. This members are set when
|
||||
// console.timeEnd() is called.
|
||||
double mStopTimerDuration;
|
||||
nsString mStopTimerLabel;
|
||||
Console::TimerStatus mStopTimerStatus;
|
||||
// ConsoleTimerLogOrEnd dictionary. This members are set when
|
||||
// console.timeEnd() or console.timeLog() are called.
|
||||
double mLogTimerDuration;
|
||||
nsString mLogTimerLabel;
|
||||
Console::TimerStatus mLogTimerStatus;
|
||||
|
||||
// These 2 values are set by IncreaseCounter on the owning thread and they are
|
||||
// used CreateCounterValue. These members are set when console.count() is
|
||||
@ -1231,30 +1231,42 @@ Console::GroupEnd(const GlobalObject& aGlobal)
|
||||
/* static */ void
|
||||
Console::Time(const GlobalObject& aGlobal, const nsAString& aLabel)
|
||||
{
|
||||
StringMethod(aGlobal, aLabel, MethodTime, NS_LITERAL_STRING("time"));
|
||||
StringMethod(aGlobal, aLabel, Sequence<JS::Value>(), MethodTime,
|
||||
NS_LITERAL_STRING("time"));
|
||||
}
|
||||
|
||||
/* static */ void
|
||||
Console::TimeEnd(const GlobalObject& aGlobal, const nsAString& aLabel)
|
||||
{
|
||||
StringMethod(aGlobal, aLabel, MethodTimeEnd, NS_LITERAL_STRING("timeEnd"));
|
||||
StringMethod(aGlobal, aLabel, Sequence<JS::Value>(), MethodTimeEnd,
|
||||
NS_LITERAL_STRING("timeEnd"));
|
||||
}
|
||||
|
||||
/* static */ void
|
||||
Console::TimeLog(const GlobalObject& aGlobal, const nsAString& aLabel,
|
||||
const Sequence<JS::Value>& aData)
|
||||
{
|
||||
StringMethod(aGlobal, aLabel, aData, MethodTimeLog,
|
||||
NS_LITERAL_STRING("timeLog"));
|
||||
}
|
||||
|
||||
/* static */ void
|
||||
Console::StringMethod(const GlobalObject& aGlobal, const nsAString& aLabel,
|
||||
MethodName aMethodName, const nsAString& aMethodString)
|
||||
const Sequence<JS::Value>& aData, MethodName aMethodName,
|
||||
const nsAString& aMethodString)
|
||||
{
|
||||
RefPtr<Console> console = GetConsole(aGlobal);
|
||||
if (!console) {
|
||||
return;
|
||||
}
|
||||
|
||||
console->StringMethodInternal(aGlobal.Context(), aLabel, aMethodName,
|
||||
console->StringMethodInternal(aGlobal.Context(), aLabel, aData, aMethodName,
|
||||
aMethodString);
|
||||
}
|
||||
|
||||
void
|
||||
Console::StringMethodInternal(JSContext* aCx, const nsAString& aLabel,
|
||||
const Sequence<JS::Value>& aData,
|
||||
MethodName aMethodName,
|
||||
const nsAString& aMethodString)
|
||||
{
|
||||
@ -1272,6 +1284,12 @@ Console::StringMethodInternal(JSContext* aCx, const nsAString& aLabel,
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < aData.Length(); ++i) {
|
||||
if (!data.AppendElement(aData[i], fallible)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
MethodInternal(aCx, aMethodName, aMethodString, data);
|
||||
}
|
||||
|
||||
@ -1422,7 +1440,8 @@ Console::Assert(const GlobalObject& aGlobal, bool aCondition,
|
||||
/* static */ void
|
||||
Console::Count(const GlobalObject& aGlobal, const nsAString& aLabel)
|
||||
{
|
||||
StringMethod(aGlobal, aLabel, MethodCount, NS_LITERAL_STRING("count"));
|
||||
StringMethod(aGlobal, aLabel, Sequence<JS::Value>(), MethodCount,
|
||||
NS_LITERAL_STRING("count"));
|
||||
}
|
||||
|
||||
namespace {
|
||||
@ -1573,8 +1592,9 @@ Console::MethodInternal(JSContext* aCx, MethodName aMethodName,
|
||||
|
||||
DOMHighResTimeStamp monotonicTimer;
|
||||
|
||||
// Monotonic timer for 'time' and 'timeEnd'
|
||||
// Monotonic timer for 'time', 'timeLog' and 'timeEnd'
|
||||
if ((aMethodName == MethodTime ||
|
||||
aMethodName == MethodTimeLog ||
|
||||
aMethodName == MethodTimeEnd ||
|
||||
aMethodName == MethodTimeStamp) &&
|
||||
!MonotonicTimer(aCx, aMethodName, aData, &monotonicTimer)) {
|
||||
@ -1589,10 +1609,19 @@ Console::MethodInternal(JSContext* aCx, MethodName aMethodName,
|
||||
}
|
||||
|
||||
else if (aMethodName == MethodTimeEnd && !aData.IsEmpty()) {
|
||||
callData->mStopTimerStatus = StopTimer(aCx, aData[0],
|
||||
monotonicTimer,
|
||||
callData->mStopTimerLabel,
|
||||
&callData->mStopTimerDuration);
|
||||
callData->mLogTimerStatus = LogTimer(aCx, aData[0],
|
||||
monotonicTimer,
|
||||
callData->mLogTimerLabel,
|
||||
&callData->mLogTimerDuration,
|
||||
true /* Cancel timer */);
|
||||
}
|
||||
|
||||
else if (aMethodName == MethodTimeLog && !aData.IsEmpty()) {
|
||||
callData->mLogTimerStatus = LogTimer(aCx, aData[0],
|
||||
monotonicTimer,
|
||||
callData->mLogTimerLabel,
|
||||
&callData->mLogTimerDuration,
|
||||
false /* Cancel timer */);
|
||||
}
|
||||
|
||||
else if (aMethodName == MethodCount) {
|
||||
@ -1863,10 +1892,11 @@ Console::PopulateConsoleNotificationInTheTargetScope(JSContext* aCx,
|
||||
aData->mStartTimerStatus);
|
||||
}
|
||||
|
||||
else if (aData->mMethodName == MethodTimeEnd && !aArguments.IsEmpty()) {
|
||||
event.mTimer = CreateStopTimerValue(aCx, aData->mStopTimerLabel,
|
||||
aData->mStopTimerDuration,
|
||||
aData->mStopTimerStatus);
|
||||
else if ((aData->mMethodName == MethodTimeEnd ||
|
||||
aData->mMethodName == MethodTimeLog) && !aArguments.IsEmpty()) {
|
||||
event.mTimer = CreateLogOrEndTimerValue(aCx, aData->mLogTimerLabel,
|
||||
aData->mLogTimerDuration,
|
||||
aData->mLogTimerStatus);
|
||||
}
|
||||
|
||||
else if (aData->mMethodName == MethodCount) {
|
||||
@ -2321,10 +2351,11 @@ Console::CreateStartTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
|
||||
}
|
||||
|
||||
Console::TimerStatus
|
||||
Console::StopTimer(JSContext* aCx, const JS::Value& aName,
|
||||
DOMHighResTimeStamp aTimestamp,
|
||||
nsAString& aTimerLabel,
|
||||
double* aTimerDuration)
|
||||
Console::LogTimer(JSContext* aCx, const JS::Value& aName,
|
||||
DOMHighResTimeStamp aTimestamp,
|
||||
nsAString& aTimerLabel,
|
||||
double* aTimerDuration,
|
||||
bool aCancelTimer)
|
||||
{
|
||||
AssertIsOnOwningThread();
|
||||
MOZ_ASSERT(aTimerDuration);
|
||||
@ -2345,9 +2376,17 @@ Console::StopTimer(JSContext* aCx, const JS::Value& aName,
|
||||
aTimerLabel = key;
|
||||
|
||||
DOMHighResTimeStamp value = 0;
|
||||
if (!mTimerRegistry.Remove(key, &value)) {
|
||||
NS_WARNING("mTimerRegistry entry not found");
|
||||
return eTimerDoesntExist;
|
||||
|
||||
if (aCancelTimer) {
|
||||
if (!mTimerRegistry.Remove(key, &value)) {
|
||||
NS_WARNING("mTimerRegistry entry not found");
|
||||
return eTimerDoesntExist;
|
||||
}
|
||||
} else {
|
||||
if (!mTimerRegistry.Get(key, &value)) {
|
||||
NS_WARNING("mTimerRegistry entry not found");
|
||||
return eTimerDoesntExist;
|
||||
}
|
||||
}
|
||||
|
||||
*aTimerDuration = aTimestamp - value;
|
||||
@ -2355,14 +2394,14 @@ Console::StopTimer(JSContext* aCx, const JS::Value& aName,
|
||||
}
|
||||
|
||||
JS::Value
|
||||
Console::CreateStopTimerValue(JSContext* aCx, const nsAString& aLabel,
|
||||
double aDuration, TimerStatus aStatus) const
|
||||
Console::CreateLogOrEndTimerValue(JSContext* aCx, const nsAString& aLabel,
|
||||
double aDuration, TimerStatus aStatus) const
|
||||
{
|
||||
if (aStatus != eTimerDone) {
|
||||
return CreateTimerError(aCx, aLabel, aStatus);
|
||||
}
|
||||
|
||||
RootedDictionary<ConsoleTimerEnd> timer(aCx);
|
||||
RootedDictionary<ConsoleTimerLogOrEnd> timer(aCx);
|
||||
timer.mName = aLabel;
|
||||
timer.mDuration = aDuration;
|
||||
|
||||
@ -2996,6 +3035,7 @@ Console::WebIDLLogLevelToInteger(ConsoleLogLevel aLevel) const
|
||||
case ConsoleLogLevel::Info: return 3;
|
||||
case ConsoleLogLevel::Clear: return 3;
|
||||
case ConsoleLogLevel::Trace: return 3;
|
||||
case ConsoleLogLevel::TimeLog: return 3;
|
||||
case ConsoleLogLevel::TimeEnd: return 3;
|
||||
case ConsoleLogLevel::Time: return 3;
|
||||
case ConsoleLogLevel::Group: return 3;
|
||||
@ -3033,6 +3073,7 @@ Console::InternalLogLevelToInteger(MethodName aName) const
|
||||
case MethodGroupCollapsed: return 3;
|
||||
case MethodGroupEnd: return 3;
|
||||
case MethodTime: return 3;
|
||||
case MethodTimeLog: return 3;
|
||||
case MethodTimeEnd: return 3;
|
||||
case MethodTimeStamp: return 3;
|
||||
case MethodAssert: return 3;
|
||||
|
@ -96,6 +96,10 @@ public:
|
||||
static void
|
||||
Time(const GlobalObject& aGlobal, const nsAString& aLabel);
|
||||
|
||||
static void
|
||||
TimeLog(const GlobalObject& aGlobal, const nsAString& aLabel,
|
||||
const Sequence<JS::Value>& aData);
|
||||
|
||||
static void
|
||||
TimeEnd(const GlobalObject& aGlobal, const nsAString& aLabel);
|
||||
|
||||
@ -159,6 +163,7 @@ private:
|
||||
MethodGroupCollapsed,
|
||||
MethodGroupEnd,
|
||||
MethodTime,
|
||||
MethodTimeLog,
|
||||
MethodTimeEnd,
|
||||
MethodTimeStamp,
|
||||
MethodAssert,
|
||||
@ -193,10 +198,12 @@ private:
|
||||
|
||||
static void
|
||||
StringMethod(const GlobalObject& aGlobal, const nsAString& aLabel,
|
||||
MethodName aMethodName, const nsAString& aMethodString);
|
||||
const Sequence<JS::Value>& aData, MethodName aMethodName,
|
||||
const nsAString& aMethodString);
|
||||
|
||||
void
|
||||
StringMethodInternal(JSContext* aCx, const nsAString& aLabel,
|
||||
const Sequence<JS::Value>& aData,
|
||||
MethodName aMethodName, const nsAString& aMethodString);
|
||||
|
||||
// This method must receive aCx and aArguments in the same JSCompartment.
|
||||
@ -317,9 +324,9 @@ private:
|
||||
CreateStartTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
|
||||
TimerStatus aTimerStatus) const;
|
||||
|
||||
// StopTimer follows the same pattern as StartTimer: it runs on the
|
||||
// LogTimer follows the same pattern as StartTimer: it runs on the
|
||||
// owning thread and populates aTimerLabel and aTimerDuration, used by
|
||||
// CreateStopTimerValue.
|
||||
// CreateLogOrEndTimerValue.
|
||||
// * aCx - the JSContext rooting aName.
|
||||
// * aName - this is (should be) the name of the timer as JS::Value.
|
||||
// * aTimestamp - the monotonicTimer for this context taken from
|
||||
@ -328,22 +335,24 @@ private:
|
||||
// string.
|
||||
// * aTimerDuration - the difference between aTimestamp and when the timer
|
||||
// started (see StartTimer).
|
||||
// * aCancelTimer - if true, the timer is removed from the table.
|
||||
TimerStatus
|
||||
StopTimer(JSContext* aCx, const JS::Value& aName,
|
||||
DOMHighResTimeStamp aTimestamp,
|
||||
nsAString& aTimerLabel,
|
||||
double* aTimerDuration);
|
||||
LogTimer(JSContext* aCx, const JS::Value& aName,
|
||||
DOMHighResTimeStamp aTimestamp,
|
||||
nsAString& aTimerLabel,
|
||||
double* aTimerDuration,
|
||||
bool aCancelTimer);
|
||||
|
||||
// This method generates a ConsoleTimerEnd dictionary exposed as JS::Value, or
|
||||
// a ConsoleTimerError dictionary if aTimerStatus is false. See StopTimer.
|
||||
// a ConsoleTimerError dictionary if aTimerStatus is false. See LogTimer.
|
||||
// * aCx - this is the context that will root the returned value.
|
||||
// * aTimerLabel - this label must be what StopTimer received as aTimerLabel.
|
||||
// * aTimerDuration - this is what StopTimer received as aTimerDuration
|
||||
// * aTimerStatus - the return value of StopTimer.
|
||||
// * aTimerLabel - this label must be what LogTimer received as aTimerLabel.
|
||||
// * aTimerDuration - this is what LogTimer received as aTimerDuration
|
||||
// * aTimerStatus - the return value of LogTimer.
|
||||
JS::Value
|
||||
CreateStopTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
|
||||
double aTimerDuration,
|
||||
TimerStatus aTimerStatus) const;
|
||||
CreateLogOrEndTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
|
||||
double aTimerDuration,
|
||||
TimerStatus aTimerStatus) const;
|
||||
|
||||
// The method populates a Sequence from an array of JS::Value.
|
||||
bool
|
||||
|
@ -147,14 +147,24 @@ ConsoleInstance::GroupEnd(JSContext* aCx)
|
||||
void
|
||||
ConsoleInstance::Time(JSContext* aCx, const nsAString& aLabel)
|
||||
{
|
||||
mConsole->StringMethodInternal(aCx, aLabel, Console::MethodTime,
|
||||
mConsole->StringMethodInternal(aCx, aLabel, Sequence<JS::Value>(),
|
||||
Console::MethodTime,
|
||||
NS_LITERAL_STRING("time"));
|
||||
}
|
||||
|
||||
void
|
||||
ConsoleInstance::TimeLog(JSContext* aCx, const nsAString& aLabel,
|
||||
const Sequence<JS::Value>& aData)
|
||||
{
|
||||
mConsole->StringMethodInternal(aCx, aLabel, aData, Console::MethodTimeLog,
|
||||
NS_LITERAL_STRING("timeLog"));
|
||||
}
|
||||
|
||||
void
|
||||
ConsoleInstance::TimeEnd(JSContext* aCx, const nsAString& aLabel)
|
||||
{
|
||||
mConsole->StringMethodInternal(aCx, aLabel, Console::MethodTimeEnd,
|
||||
mConsole->StringMethodInternal(aCx, aLabel, Sequence<JS::Value>(),
|
||||
Console::MethodTimeEnd,
|
||||
NS_LITERAL_STRING("timeEnd"));
|
||||
}
|
||||
|
||||
@ -201,7 +211,8 @@ ConsoleInstance::Assert(JSContext* aCx, bool aCondition,
|
||||
void
|
||||
ConsoleInstance::Count(JSContext* aCx, const nsAString& aLabel)
|
||||
{
|
||||
mConsole->StringMethodInternal(aCx, aLabel, Console::MethodCount,
|
||||
mConsole->StringMethodInternal(aCx, aLabel, Sequence<JS::Value>(),
|
||||
Console::MethodCount,
|
||||
NS_LITERAL_STRING("count"));
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,10 @@ public:
|
||||
void
|
||||
Time(JSContext* aCx, const nsAString& aLabel);
|
||||
|
||||
void
|
||||
TimeLog(JSContext* aCx, const nsAString& aLabel,
|
||||
const Sequence<JS::Value>& aData);
|
||||
|
||||
void
|
||||
TimeEnd(JSContext* aCx, const nsAString& aLabel);
|
||||
|
||||
|
@ -10,3 +10,4 @@ support-files =
|
||||
[test_console_binding.html]
|
||||
[test_console_proto.html]
|
||||
[test_devtools_pref.html]
|
||||
[test_timer.html]
|
||||
|
84
dom/console/tests/test_timer.html
Normal file
84
dom/console/tests/test_timer.html
Normal file
@ -0,0 +1,84 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test for timeStart/timeLog/timeEnd in console</title>
|
||||
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<script type="application/javascript">
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
function consoleListener(cb) {
|
||||
return new Promise(resolve => {
|
||||
let observer = {
|
||||
observe: function listener(aSubject, aTopic, aData) {
|
||||
var obj = aSubject.wrappedJSObject;
|
||||
if (obj.arguments[0] == 'test' && cb(obj)) {
|
||||
SpecialPowers.removeObserver(observer, "console-api-log-event");
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
};
|
||||
SpecialPowers.addObserver(observer, "console-api-log-event");
|
||||
});
|
||||
}
|
||||
|
||||
// Timer creation:
|
||||
async function runTest() {
|
||||
var cl = consoleListener(function(obj) {
|
||||
return ("timer" in obj) &&
|
||||
("name" in obj.timer) &&
|
||||
obj.timer.name == 'test';
|
||||
});
|
||||
console.time('test');
|
||||
await cl;
|
||||
ok(true, "Console.time received!");
|
||||
|
||||
// Timer check:
|
||||
cl = consoleListener(obj => {
|
||||
return ("timer" in obj) &&
|
||||
("name" in obj.timer) &&
|
||||
obj.timer.name == 'test' &&
|
||||
("duration" in obj.timer) &&
|
||||
obj.timer.duration > 0 &&
|
||||
obj.arguments[1] == 1 &&
|
||||
obj.arguments[2] == 2 &&
|
||||
obj.arguments[3] == 3 &&
|
||||
obj.arguments[4] == 4;
|
||||
});
|
||||
console.timeLog('test', 1, 2, 3, 4);
|
||||
await cl;
|
||||
ok(true, "Console.timeLog received!");
|
||||
|
||||
// Time deleted:
|
||||
cl = consoleListener(obj => {
|
||||
return ("timer" in obj) &&
|
||||
("name" in obj.timer) &&
|
||||
obj.timer.name == 'test' &&
|
||||
("duration" in obj.timer) &&
|
||||
obj.timer.duration > 0;
|
||||
});
|
||||
console.timeEnd('test');
|
||||
await cl;
|
||||
ok(true, "Console.timeEnd received!");
|
||||
|
||||
// Here an error:
|
||||
cl = consoleListener(obj => {
|
||||
return ("timer" in obj) &&
|
||||
("name" in obj.timer) &&
|
||||
obj.timer.name == 'test' &&
|
||||
("error" in obj.timer);
|
||||
});
|
||||
console.timeLog('test');
|
||||
await cl;
|
||||
ok(true, "Console.timeLog with error received!");
|
||||
}
|
||||
|
||||
runTest().then(SimpleTest.finish);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -32,6 +32,7 @@ function doTest() {
|
||||
"groupCollapsed": "function",
|
||||
"groupEnd": "function",
|
||||
"time": "function",
|
||||
"timeLog": "function",
|
||||
"timeEnd": "function",
|
||||
"profile": "function",
|
||||
"profileEnd": "function",
|
||||
|
@ -54,6 +54,8 @@ namespace console {
|
||||
[UseCounter]
|
||||
void time(optional DOMString label = "default");
|
||||
[UseCounter]
|
||||
void timeLog(optional DOMString label = "default", any... data);
|
||||
[UseCounter]
|
||||
void timeEnd(optional DOMString label = "default");
|
||||
|
||||
// Mozilla only or Webcompat methods
|
||||
@ -121,7 +123,7 @@ dictionary ConsoleTimerStart {
|
||||
DOMString name = "";
|
||||
};
|
||||
|
||||
dictionary ConsoleTimerEnd {
|
||||
dictionary ConsoleTimerLogOrEnd {
|
||||
DOMString name = "";
|
||||
double duration = 0;
|
||||
};
|
||||
@ -165,6 +167,7 @@ interface ConsoleInstance {
|
||||
|
||||
// Timing
|
||||
void time(optional DOMString label = "default");
|
||||
void timeLog(optional DOMString label = "default", any... data);
|
||||
void timeEnd(optional DOMString label = "default");
|
||||
|
||||
// Mozilla only or Webcompat methods
|
||||
@ -179,8 +182,9 @@ interface ConsoleInstance {
|
||||
callback ConsoleInstanceDumpCallback = void (DOMString message);
|
||||
|
||||
enum ConsoleLogLevel {
|
||||
"All", "Debug", "Log", "Info", "Clear", "Trace", "TimeEnd", "Time", "Group",
|
||||
"GroupEnd", "Profile", "ProfileEnd", "Dir", "Dirxml", "Warn", "Error", "Off"
|
||||
"All", "Debug", "Log", "Info", "Clear", "Trace", "TimeLog", "TimeEnd", "Time",
|
||||
"Group", "GroupEnd", "Profile", "ProfileEnd", "Dir", "Dirxml", "Warn", "Error",
|
||||
"Off"
|
||||
};
|
||||
|
||||
dictionary ConsoleInstanceOptions {
|
||||
|
Loading…
Reference in New Issue
Block a user