import changes from `dev' branch of rmottola/Arctic-Fox:

- Bug 1167459 - Skip rendering function name nodes if there's no name available (e.g. for C++ pseudoframes), r=jsantell (4a69ed224)
- missing bits Bug 1102219 - Part 4: Replace String.prototype.contains with `String.prototype.includes` in chrome code. r=till (73cd2d2b1)
- Bug 1165045 - Don't create nodes with empty text in the call tree, r=jsantell (b013aa82d)
- Bug 1166122 - Fix regression in the call tree caused by bug 1165045, r=jsantell (ee3f16901)
- Bug 1167975 - CallView._displaySelf sets this.document just because other functions use it; it should pass it as an argument instead, r=jsantell (5ef560c4f)
- Bug 1122662 - Resize graphs when window resizes;r=vporof (25c108e4e)
- Bug 1164784 - Eliminate CSS duplication with perf tool record button r=jsantell (43c9bb999)
- Bug 1150761 - Rename the performance tool's details view names to better describe the data visualizations. r=vp (04ceb6a37)
- Bug 1144424 - Rename '{self,total} allocations' to '{self,total} sampled allocations' in the performance tool. r=jsantell (ae79ad54f)
- Bug 1069910 - Add tooltips explaining what each column in the profiler's tree view represents; r=jsantell (8756f88b6)
- Bug 1107849 - Define a min/max width for the performance panel sidebar. r=vporof (e1769e831)
- Bug 11663354 - A locked recording button should appear disabled in the performance tool. r=vp (4a359d39e)
- Bug 1023546 - DevTools - Support HDPI resolutions for Windows. r=bgrins (ef1a3ecb8)
- Bug 1168125 - Cleanup performance xul and css, r=jsantell (8ec794e46)
- Bug 1168125 - Replace the waterfall view with a tree, r=jsantell (ea76514fe)
- Bug 1168125 - Add marker folding logic, r=jsantell (1d3748d2a)
- Bug 862341 Part 1: Move the network request storage from the console frontend to the console client so the netmonitor can reuse it. r=vporof (d29fb2b73)
- remove gre from resource path (126b00df1)
- Bug 943306 - Allow persisting console input history between sessions;r=past (146ebb486)
- Bug 1134845 - Add clearHistory jsterm helper to remove persisted console input history. r=past (22237e95b)
- Bug 1143497 - Offer a way to extend WebConsole commands. r=bgrins (84e2d2957)
- Bug 1125205 - Display console API messages from shared or service workers to the web console, r=past (b4b701a2c)
- Bug 1169342 - Remove nsIDOMDeviceStorage. Cleanup nsDOMDeviceStorage event wrappers. r=dhylands (41338e16f)
- Bug 1151610 - Manage the case where two extensions fight over the same command. r=bgrins (63f9d2064)
- Bug 862341 Part 2: Display cached network requests in the web console. r=vporof (83c0e7263)
- Bug 1144211 - Improve code coverage of camera mochitests. r=mikeh (ba9f3de89)
- Bug 1152500 - Fix how stop recording may be handled out-of-order. r=dhylands (d8bdd379c)
- Bug 862341 Part 3: Display cached network requests in the network panel. r=vporof (a1a6f151d)
- Bug 862341 Part 4: Start recording network requests when the toolbox opens. r=vporof (7a2bdf847)
- Bug 1151499 - Correct the FM playable state. r=baku (8af26fff2)
- Bug 1180347 - Split media.useAudioChannelService to support turning the service on without turning the Firefox OS specific APIs on; r=baku (3fa29291a)
- Bug 862341 Part 5: Tests. r=vporof (82fb944c6)
This commit is contained in:
2021-05-29 22:53:37 +08:00
parent 98894236c9
commit b9843e0358
100 changed files with 4707 additions and 2466 deletions
+2
View File
@@ -856,6 +856,8 @@ pref("general.useragent.device_id", "");
// Make <audio> and <video> talk to the AudioChannelService.
pref("media.useAudioChannelService", true);
// Add Mozilla AudioChannel APIs.
pref("media.useAudioChannelAPI", true);
pref("b2g.version", @MOZ_B2G_VERSION@);
pref("b2g.osName", @MOZ_B2G_OS_NAME@);
-1
View File
@@ -193,7 +193,6 @@
@RESPATH@/components/dom_alarm.xpt
@RESPATH@/components/dom_core.xpt
@RESPATH@/components/dom_css.xpt
@RESPATH@/components/dom_devicestorage.xpt
@RESPATH@/components/dom_events.xpt
@RESPATH@/components/dom_geolocation.xpt
@RESPATH@/components/dom_media.xpt
+11
View File
@@ -1110,6 +1110,17 @@ pref("devtools.gcli.imgurClientID", '0df414e888d7240');
// Imgur's upload URL
pref("devtools.gcli.imgurUploadURL", "https://api.imgur.com/3/image");
pref("devtools.webconsole.filter.serviceworkers", false);
pref("devtools.webconsole.filter.sharedworkers", false);
pref("devtools.webconsole.filter.windowlessworkers", false);
pref("devtools.browserconsole.filter.serviceworkers", true);
pref("devtools.browserconsole.filter.sharedworkers", true);
pref("devtools.browserconsole.filter.windowlessworkers", true);
// Max number of inputs to store in web console history.
pref("devtools.webconsole.inputHistoryCount", 50);
// Whether the character encoding menu is under the main Firefox button. This
// preference is a string so that localizers can alter it.
pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties");
-1
View File
@@ -216,7 +216,6 @@
@RESPATH@/components/dom_alarm.xpt
@RESPATH@/components/dom_core.xpt
@RESPATH@/components/dom_css.xpt
@RESPATH@/components/dom_devicestorage.xpt
@RESPATH@/components/dom_events.xpt
@RESPATH@/components/dom_geolocation.xpt
@RESPATH@/components/dom_media.xpt
@@ -52,6 +52,7 @@ var tests = [
// Preferences
function() {
SpecialPowers.pushPrefEnv({"set": [["dom.ipc.browser_frames.oop_by_default", true],
["media.useAudioChannelAPI", true],
["media.useAudioChannelService", true],
["media.defaultAudioChannel", "telephony"],
["dom.mozBrowserFramesEnabled", true],
+1 -1
View File
@@ -469,7 +469,7 @@ private:
id = NS_LITERAL_STRING("Worker");
}
mCallData->SetIDs(id, frame.mFilename);
mCallData->SetIDs(frame.mFilename, id);
}
// Now we could have the correct window (if we are not window-less).
-1
View File
@@ -12,7 +12,6 @@
#include "nsIFile.h"
#include "nsProxyRelease.h"
#include "AutoRwLock.h"
#include "nsIDOMDeviceStorage.h"
#include "ICameraControl.h"
#include "CameraCommon.h"
#include "DeviceStorage.h"
+9 -3
View File
@@ -74,9 +74,15 @@ MozCameraTestHardware.prototype = {
},
dispatchEvent: function(evt) {
if (this._handler) {
this._handler.handleEvent(evt);
}
var self = this;
/* We should not dispatch the event in the current thread
context because it may be directly from a driver call
and we could hit a deadlock situation. */
this._window.setTimeout(function() {
if (self._handler) {
self._handler.handleEvent(evt);
}
}, 0);
},
reset: function(aWindow) {
+74 -37
View File
@@ -18,7 +18,6 @@
#include "mozilla/unused.h"
#include "nsIAppsService.h"
#include "nsIObserverService.h"
#include "nsIDOMDeviceStorage.h"
#include "nsIDOMEventListener.h"
#include "nsIScriptSecurityManager.h"
#include "Navigator.h"
@@ -202,6 +201,7 @@ nsDOMCameraControl::nsDOMCameraControl(uint32_t aCameraId,
, mGetCameraPromise(aPromise)
, mWindow(aWindow)
, mPreviewState(CameraControlListener::kPreviewStopped)
, mRecording(false)
, mSetInitialConfig(false)
{
DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
@@ -743,40 +743,39 @@ nsDOMCameraControl::StartRecording(const CameraStartRecordingOptions& aOptions,
return nullptr;
}
if (mStartRecordingPromise) {
if (mStartRecordingPromise || mRecording) {
promise->MaybeReject(NS_ERROR_IN_PROGRESS);
return promise.forget();
}
NotifyRecordingStatusChange(NS_LITERAL_STRING("starting"));
#ifdef MOZ_B2G
if (!mAudioChannelAgent) {
mAudioChannelAgent = do_CreateInstance("@mozilla.org/audiochannelagent;1");
if (mAudioChannelAgent) {
// Camera app will stop recording when it falls to the background, so no callback is necessary.
mAudioChannelAgent->Init(mWindow, (int32_t)AudioChannel::Content, nullptr);
// Video recording doesn't output any sound, so it's not necessary to check canPlay.
int32_t canPlay;
mAudioChannelAgent->StartPlaying(&canPlay);
}
}
#endif
nsCOMPtr<nsIDOMDOMRequest> request;
mDSFileDescriptor = new DeviceStorageFileDescriptor();
aRv = aStorageArea.CreateFileDescriptor(aFilename, mDSFileDescriptor.get(),
getter_AddRefs(request));
aRv = NotifyRecordingStatusChange(NS_LITERAL_STRING("starting"));
if (aRv.Failed()) {
return nullptr;
}
mDSFileDescriptor = new DeviceStorageFileDescriptor();
nsRefPtr<DOMRequest> request = aStorageArea.CreateFileDescriptor(aFilename,
mDSFileDescriptor.get(),
aRv);
if (aRv.Failed()) {
NotifyRecordingStatusChange(NS_LITERAL_STRING("shutdown"));
return nullptr;
}
mStartRecordingPromise = promise;
mOptions = aOptions;
EventListenerManager* elm = request->GetOrCreateListenerManager();
if (!elm) {
NotifyRecordingStatusChange(NS_LITERAL_STRING("shutdown"));
aRv.Throw(NS_ERROR_UNEXPECTED);
return nullptr;
}
mRecording = true;
nsCOMPtr<nsIDOMEventListener> listener = new StartRecordingHelper(this);
request->AddEventListener(NS_LITERAL_STRING("success"), listener, false);
request->AddEventListener(NS_LITERAL_STRING("error"), listener, false);
elm->AddEventListener(NS_LITERAL_STRING("success"), listener, false, false);
elm->AddEventListener(NS_LITERAL_STRING("error"), listener, false, false);
return promise.forget();
}
@@ -787,6 +786,10 @@ nsDOMCameraControl::OnCreatedFileDescriptor(bool aSucceeded)
if (!mCameraControl) {
rv = NS_ERROR_NOT_AVAILABLE;
} else if (!mRecording) {
// Race condition where StopRecording comes in before we issue
// the start recording request to Gonk
rv = NS_ERROR_ABORT;
} else if (aSucceeded && mDSFileDescriptor->mFileDescriptor.IsValid()) {
ICameraControl::StartRecordingOptions o;
@@ -818,13 +821,8 @@ nsDOMCameraControl::StopRecording(ErrorResult& aRv)
DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
THROW_IF_NO_CAMERACONTROL();
#ifdef MOZ_B2G
if (mAudioChannelAgent) {
mAudioChannelAgent->StopPlaying();
mAudioChannelAgent = nullptr;
}
#endif
ReleaseAudioChannelAgent();
mRecording = false;
aRv = mCameraControl->StopRecording();
}
@@ -958,8 +956,10 @@ nsDOMCameraControl::TakePicture(const CameraPictureOptions& aOptions,
if (s.width && s.height) {
mCameraControl->Set(CAMERA_PARAM_PICTURE_SIZE, s);
}
if (!aOptions.mFileFormat.IsEmpty()) {
mCameraControl->Set(CAMERA_PARAM_PICTURE_FILEFORMAT, aOptions.mFileFormat);
}
mCameraControl->Set(CAMERA_PARAM_PICTURE_ROTATION, aOptions.mRotation);
mCameraControl->Set(CAMERA_PARAM_PICTURE_FILEFORMAT, aOptions.mFileFormat);
mCameraControl->Set(CAMERA_PARAM_PICTURE_DATETIME, aOptions.mDateTime);
mCameraControl->SetLocation(p);
}
@@ -1032,15 +1032,50 @@ nsDOMCameraControl::Shutdown()
}
}
void
nsDOMCameraControl::ReleaseAudioChannelAgent()
{
#ifdef MOZ_B2G
if (mAudioChannelAgent) {
mAudioChannelAgent->StopPlaying();
mAudioChannelAgent = nullptr;
}
#endif
}
nsresult
nsDOMCameraControl::NotifyRecordingStatusChange(const nsString& aMsg)
{
NS_ENSURE_TRUE(mWindow, NS_ERROR_FAILURE);
return MediaManager::NotifyRecordingStatusChange(mWindow,
aMsg,
true /* aIsAudio */,
true /* aIsVideo */);
if (aMsg.EqualsLiteral("shutdown")) {
ReleaseAudioChannelAgent();
}
nsresult rv = MediaManager::NotifyRecordingStatusChange(mWindow,
aMsg,
true /* aIsAudio */,
true /* aIsVideo */);
if (NS_FAILED(rv)) {
return rv;
}
#ifdef MOZ_B2G
if (aMsg.EqualsLiteral("starting") && !mAudioChannelAgent) {
mAudioChannelAgent = do_CreateInstance("@mozilla.org/audiochannelagent;1");
if (!mAudioChannelAgent) {
return NS_ERROR_UNEXPECTED;
}
// Camera app will stop recording when it falls to the background, so no callback is necessary.
mAudioChannelAgent->Init(mWindow, (int32_t)AudioChannel::Content, nullptr);
// Video recording doesn't output any sound, so it's not necessary to check canPlay.
int32_t canPlay;
mAudioChannelAgent->StartPlaying(&canPlay);
}
#endif
return rv;
}
already_AddRefed<Promise>
@@ -1224,7 +1259,7 @@ nsDOMCameraControl::OnRecorderStateChange(CameraControlListener::RecorderState a
int32_t aArg, int32_t aTrackNum)
{
// For now, we do nothing with 'aStatus' and 'aTrackNum'.
DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
DOM_CAMERA_LOGT("%s:%d : this=%p, state=%u\n", __func__, __LINE__, this, aState);
MOZ_ASSERT(NS_IsMainThread());
ErrorResult ignored;
@@ -1417,7 +1452,7 @@ nsDOMCameraControl::OnTakePictureComplete(nsIDOMBlob* aPicture)
void
nsDOMCameraControl::OnUserError(CameraControlListener::UserContext aContext, nsresult aError)
{
DOM_CAMERA_LOGI("DOM OnUserError : this=%paContext=%u, aError=0x%x\n",
DOM_CAMERA_LOGI("DOM OnUserError : this=%p, aContext=%u, aError=0x%x\n",
this, aContext, aError);
MOZ_ASSERT(NS_IsMainThread());
@@ -1470,6 +1505,8 @@ nsDOMCameraControl::OnUserError(CameraControlListener::UserContext aContext, nsr
case CameraControlListener::kInStartRecording:
promise = mStartRecordingPromise.forget();
mRecording = false;
NotifyRecordingStatusChange(NS_LITERAL_STRING("shutdown"));
break;
case CameraControlListener::kInStartFaceDetection:
+2 -1
View File
@@ -182,6 +182,7 @@ protected:
bool IsWindowStillActive();
nsresult SelectPreviewSize(const dom::CameraSize& aRequestedPreviewSize, ICameraControl::Size& aSelectedPreviewSize);
void ReleaseAudioChannelAgent();
nsresult NotifyRecordingStatusChange(const nsString& aMsg);
already_AddRefed<dom::Promise> CreatePromise(ErrorResult& aRv);
@@ -223,7 +224,7 @@ protected:
dom::CameraStartRecordingOptions mOptions;
nsRefPtr<DeviceStorageFileDescriptor> mDSFileDescriptor;
DOMCameraControlListener::PreviewState mPreviewState;
bool mRecording;
bool mSetInitialConfig;
#ifdef MOZ_WIDGET_GONK
+4
View File
@@ -269,6 +269,8 @@ namespace android {
return 320;
} else if (strcmp(aParameter, "vid.height") == 0) {
return 240;
} else if (strcmp(aParameter, "vid.fps") == 0) {
return 30;
}
return 0;
case CAMCORDER_QUALITY_HIGH:
@@ -277,6 +279,8 @@ namespace android {
return 640;
} else if (strcmp(aParameter, "vid.height") == 0) {
return 480;
} else if (strcmp(aParameter, "vid.fps") == 0) {
return 30;
}
return 0;
default:
+7 -5
View File
@@ -1238,7 +1238,7 @@ nsGonkCameraControl::StopRecordingImpl()
class RecordingComplete : public nsRunnable
{
public:
RecordingComplete(DeviceStorageFile* aFile)
RecordingComplete(already_AddRefed<DeviceStorageFile> aFile)
: mFile(aFile)
{ }
@@ -1268,6 +1268,11 @@ nsGonkCameraControl::StopRecordingImpl()
mRecorder->stop();
mRecorder = nullptr;
#else
if (!mVideoFile) {
return NS_OK;
}
#endif
OnRecorderStateChange(CameraControlListener::kRecorderStopped);
{
@@ -1282,10 +1287,7 @@ nsGonkCameraControl::StopRecordingImpl()
}
// notify DeviceStorage that the new video file is closed and ready
return NS_DispatchToMainThread(new RecordingComplete(mVideoFile));
#else
return NS_OK;
#endif
return NS_DispatchToMainThread(new RecordingComplete(mVideoFile.forget()));
}
nsresult
+26
View File
@@ -416,6 +416,32 @@ TestGonkCameraHardware::AutoFocus()
return OK;
}
int
TestGonkCameraHardware::CancelAutoFocus()
{
class Delegate : public ControlMessage
{
public:
Delegate(TestGonkCameraHardware* aTestHw)
: ControlMessage(aTestHw)
{ }
protected:
NS_IMETHOD
RunImpl() override
{
return mJSTestWrapper->CancelAutoFocus();
}
};
DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
nsresult rv = WaitWhileRunningOnMainThread(new Delegate(this));
if (NS_WARN_IF(NS_FAILED(rv))) {
return UNKNOWN_ERROR;
}
return OK;
}
int
TestGonkCameraHardware::StartFaceDetection()
{
+1
View File
@@ -33,6 +33,7 @@ class TestGonkCameraHardware : public android::GonkCameraHardware
public:
virtual nsresult Init() override;
virtual int AutoFocus() override;
virtual int CancelAutoFocus() override;
virtual int StartFaceDetection() override;
virtual int StopFaceDetection() override;
virtual int TakePicture() override;
+20 -8
View File
@@ -103,6 +103,14 @@ CameraTestSuite.prototype = {
_lowMemSet: false,
_reloading: false,
_setupPermission: function(permission) {
if (!SpecialPowers.hasPermission(permission, document)) {
info("requesting " + permission + " permission");
SpecialPowers.addPermission(permission, true, document);
this._reloading = true;
}
},
/* Returns a promise which is resolved when the test suite is ready
to be executing individual test cases. One may provide the expected
hardware type here if desired; the default is to use the JS test
@@ -111,24 +119,28 @@ CameraTestSuite.prototype = {
/* Depending on how we run the mochitest, we may not have the necessary
permissions yet. If we do need to request them, then we have to reload
the window to ensure the reconfiguration propogated properly. */
if (!SpecialPowers.hasPermission("camera", document)) {
info("requesting camera permission");
this._reloading = true;
SpecialPowers.addPermission("camera", true, document);
this._setupPermission("camera");
this._setupPermission("device-storage:videos");
this._setupPermission("device-storage:videos-create");
this._setupPermission("device-storage:videos-write");
if (this._reloading) {
window.location.reload();
return Promise.reject();
}
info("has camera permission");
info("has necessary permissions");
if (!isDefined(hwType)) {
hwType = 'hardware';
}
this._hwType = hwType;
return new Promise(function(resolve, reject) {
SpecialPowers.pushPrefEnv({'set': [['camera.control.test.permission', true]]}, function() {
SpecialPowers.pushPrefEnv({'set': [['camera.control.test.enabled', hwType]]}, function() {
resolve();
SpecialPowers.pushPrefEnv({'set': [['device.storage.prompt.testing', true]]}, function() {
SpecialPowers.pushPrefEnv({'set': [['camera.control.test.permission', true]]}, function() {
SpecialPowers.pushPrefEnv({'set': [['camera.control.test.enabled', hwType]]}, function() {
resolve();
});
});
});
});
+9 -10
View File
@@ -1,21 +1,20 @@
[DEFAULT]
support-files = camera_common.js
[test_camera_configuration.html]
[test_camera_release.html]
[test_camera_auto_focus.html]
[test_camera_take_picture.html]
[test_camera_record.html]
skip-if = toolkit == 'gonk'
[test_camera_face_detection.html]
[test_camera_fake_parameters.html]
[test_camera_hardware_init_failure.html]
[test_camera.html]
skip-if = toolkit != 'gonk'
[test_camera_2.html]
skip-if = toolkit != 'gonk'
[test_camera_3.html]
skip-if = toolkit != 'gonk'
[test_camera_hardware_init_failure.html]
[test_camera_hardware_failures.html]
[test_bug975472.html]
[test_camera_fake_parameters.html]
[test_camera_hardware_face_detection.html]
[test_camera_hardware_auto_focus_moving_cb.html]
[test_bug1022766.html]
[test_bug1037322.html]
[test_bug1099390.html]
[test_bug1104913.html]
skip-if = toolkit != 'gonk'
[test_camera_bad_initial_config.html]
-63
View File
@@ -1,63 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for bug 1022766</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="camera_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<video id="viewfinder" width="200" height="200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<script class="testbody" type="text/javascript;version=1.7">
var suite = new CameraTestSuite();
suite.test('bug-1022766', function() {
function triggerAutoFocus(p) {
return new Promise(function(resolve, reject) {
var firstCall = false;
var secondCall = false;
function end() {
if (firstCall && secondCall) {
resolve();
}
}
// It doesn't matter if the emulator supports focus or not;
// this is just testing the sequencing.
suite.camera.autoFocus().then(function(p) {
ok(false, "First call to autoFocus() succeeded unexpectedly");
firstCall = true;
end();
}, function(e) {
ok(e.name === 'NS_ERROR_IN_PROGRESS', 'First call to autoFocus() failed with: ' + e);
firstCall = true;
end();
});
suite.camera.autoFocus().then(function(p) {
ok(true, "Second call to autoFocus() succeeded");
secondCall = true;
end();
}, function(e) {
ok(false, "Second call to autoFocus() failed unexpectedly with: " + e);
secondCall = true;
end();
});
});
}
return suite.getCamera()
.then(triggerAutoFocus, suite.rejectGetCamera)
});
suite.setup()
.then(suite.run);
</script>
</body>
</html>
-69
View File
@@ -1,69 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for bug 1037322</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="camera_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<video id="viewfinder" width="200" height="200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<script class="testbody" type="text/javascript;version=1.7">
var suite = new CameraTestSuite();
suite.test('bug-1037322', function() {
var cameraManager = navigator.mozCameras;
var whichCamera = cameraManager.getListOfCameras()[0];
var postConfig = {
mode: 'picture',
recorderProfile: 'low',
previewSize: {
width: 320,
height: 240
}
};
function resolveGetCamera(p) {
suite.camera = p.camera;
// Check the default configuration
var cfg = p.configuration;
ok(cfg.mode === "unspecified", "Initial mode = " + cfg.mode);
ok(cfg.previewSize.width === 0 && cfg.previewSize.height === 0,
"Initial preview size = " + cfg.previewSize.width + "x" + cfg.previewSize.height);
ok(cfg.recorderProfile === "default",
"Initial recorder profile = '" + cfg.recorderProfile + "'");
}
function configure(p) {
// Apply our specific configuration
return suite.camera.setConfiguration(postConfig);
}
function resolveConfigure(cfg) {
// Check our specific configuration
ok(cfg.mode === postConfig.mode, "Configured mode = " + cfg.mode);
ok(cfg.previewSize.width === postConfig.previewSize.width &&
cfg.previewSize.height === postConfig.previewSize.height,
"Configured preview size = " + cfg.previewSize.width + "x" + cfg.previewSize.height);
ok(cfg.recorderProfile === postConfig.recorderProfile,
"Configured recorder profile = '" + cfg.recorderProfile + "'");
}
return cameraManager.getCamera(whichCamera, {mode: 'unspecified'})
.then(resolveGetCamera, suite.rejectGetCamera)
.then(configure)
.then(resolveConfigure, suite.rejectConfigure);
});
suite.setup()
.then(suite.run);
</script>
</body>
</html>
-55
View File
@@ -1,55 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for bug 1099390</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="camera_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<video id="viewfinder" width="200" height="200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<script class="testbody" type="text/javascript;version=1.7">
var suite = new CameraTestSuite();
suite.test('bug-1099390', function() {
function release(p) {
return new Promise(function(resolve, reject) {
var gotCloseEvent = false;
var gotReleasePromise = false;
var onClosed = function(e) {
suite.camera.removeEventListener('close', onClosed);
ok(!gotCloseEvent, "gotCloseEvent was " + gotCloseEvent);
ok(e.reason === "HardwareReleased", "'close' event reason is: " + e.reason);
gotCloseEvent = true;
if (gotReleasePromise) {
resolve();
}
};
suite.camera.addEventListener('close', onClosed);
suite.camera.release().then(function(p) {
ok(true, "released camera");
gotReleasePromise = true;
if (gotCloseEvent) {
resolve();
}
}).catch(reject);
});
}
return suite.getCamera()
.then(release, suite.rejectGetCamera);
});
suite.setup()
.then(suite.run);
</script>
</body>
</html>
@@ -1,20 +1,15 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=940424
-->
<head>
<title>Bug 940424 - Test camera hardware API failure handling</title>
<title>Test for auto focus</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="camera_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=940424">Mozilla Bug 940424</a>
<video id="viewfinder" width = "200" height = "200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<video id="viewfinder" width="200" height="200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<script class="testbody" type="text/javascript;version=1.7">
var suite = new CameraTestSuite();
@@ -129,39 +124,109 @@ suite.test('auto-focus-failures', function() {
.then(suite.expectedRejectAutoFocus, rejectAutoFocusError)
});
suite.test('take-picture-failures', function() {
function startTakePictureProcessError(p) {
suite.hw.attach({
takePicture: function() {
suite.hw.fireTakePictureError();
suite.test('auto-focus-moving', function() {
function triggerAutoFocusMoving(p) {
var sync = new Promise(function(resolve, reject) {
function onEvent(e) {
suite.camera.removeEventListener('focus', onEvent);
ok(e.newState === 'focusing', 'autofocus event state focusing == ' + e.newState);
resolve();
}
suite.camera.addEventListener('focus', onEvent);
});
return suite.camera.takePicture();
suite.hw.fireAutoFocusMoving(true);
return sync;
}
function rejectTakePictureProcessError(e) {
ok(e.name === 'NS_ERROR_FAILURE', 'takePicture() process should fail: ' + e);
}
function startTakePictureError(p) {
suite.hw.attach({
takePicture: function() {
throw SpecialPowers.Cr.NS_ERROR_FAILURE;
function waitAutoFocusComplete(p) {
var sync = new Promise(function(resolve, reject) {
function onEvent(e) {
suite.camera.removeEventListener('focus', onEvent);
ok(e.newState === 'focused', 'autofocus event state focused == ' + e.newState);
resolve();
}
suite.camera.addEventListener('focus', onEvent);
});
return suite.camera.takePicture();
// Missing the fireAutoFocusComplete but it should timeout on its own
suite.hw.fireAutoFocusMoving(false);
return sync;
}
function rejectTakePictureError(e) {
ok(e.name === 'NS_ERROR_FAILURE', 'takePicture() should fail: ' + e);
function runAutoFocusCycle(p) {
return triggerAutoFocusMoving(p)
.then(waitAutoFocusComplete);
}
/* If the driver doesn't supply an onAutoFocusComplete notification,
gecko will timeout and provide it. After three times, it will no
longer rely upon a timeout and fire it immediately. */
return suite.getCamera()
.then(runAutoFocusCycle)
.then(runAutoFocusCycle)
.then(runAutoFocusCycle)
.then(runAutoFocusCycle);
});
suite.test('auto-focus-interrupted', function() {
// bug 1022766
function triggerAutoFocus(p) {
return new Promise(function(resolve, reject) {
var firstCall = false;
var secondCall = false;
function end() {
if (firstCall && secondCall) {
resolve();
}
}
// It doesn't matter if the emulator supports focus or not;
// this is just testing the sequencing.
suite.camera.autoFocus().then(function(p) {
ok(false, "First call to autoFocus() succeeded unexpectedly");
firstCall = true;
end();
}, function(e) {
ok(e.name === 'NS_ERROR_IN_PROGRESS', 'First call to autoFocus() failed with: ' + e);
firstCall = true;
end();
});
suite.camera.autoFocus().then(function(p) {
ok(true, "Second call to autoFocus() succeeded");
secondCall = true;
end();
}, function(e) {
ok(false, "Second call to autoFocus() failed unexpectedly with: " + e);
secondCall = true;
end();
});
});
}
return suite.getCamera()
.catch(suite.rejectGetCamera)
.then(startTakePictureProcessError)
.then(suite.expectedRejectTakePicture, rejectTakePictureProcessError)
.then(startTakePictureError)
.then(suite.expectedRejectTakePicture, rejectTakePictureError)
.then(triggerAutoFocus, suite.rejectGetCamera)
});
suite.test('cancel-auto-focus', function() {
function cancelAutoFocus(p) {
var promise = new Promise(function(resolve, reject) {
suite.hw.attach({
cancelAutoFocus: function() {
ok(true, 'got cancel auto focus');
resolve();
}
});
});
suite.camera.resumeContinuousFocus();
return promise;
}
return suite.getCamera()
.then(cancelAutoFocus, suite.rejectGetCamera);
});
suite.setup()
@@ -1,44 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for bad initial configuration</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="camera_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<video id="viewfinder" width="200" height="200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<script class="testbody" type="text/javascript;version=1.7">
var suite = new CameraTestSuite();
suite.test('bad-initial-config', function() {
function getCamera() {
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var config = {
mode: 'picture',
recorderProfile: 'foobar',
};
return navigator.mozCameras.getCamera(whichCamera, config);
}
function rejectGetCamera(error) {
ok(error.name === "NS_ERROR_NOT_AVAILABLE",
"getCamera() failed with: " + error.name);
return Promise.resolve();
}
return getCamera()
.then(suite.expectedRejectGetCamera, rejectGetCamera);
});
suite.setup()
.then(suite.run);
</script>
</body>
</html>
@@ -0,0 +1,545 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for camera configuration</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="camera_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<video id="viewfinder" width="200" height="200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<script class="testbody" type="text/javascript;version=1.7">
var suite = new CameraTestSuite();
function verifyConfig(cfg, expConfig)
{
ok(cfg.mode === expConfig.mode, "Configured mode = " + cfg.mode +
", expected = " + expConfig.mode);
ok(cfg.previewSize.width === expConfig.previewSize.width &&
cfg.previewSize.height === expConfig.previewSize.height,
"Configured preview size = " + cfg.previewSize.width + "x" + cfg.previewSize.height +
", expected = " + expConfig.previewSize.width + "x" + expConfig.previewSize.height);
ok(cfg.pictureSize.width === expConfig.pictureSize.width &&
cfg.pictureSize.height === expConfig.pictureSize.height,
"Configured picture size = " + cfg.pictureSize.width + "x" + cfg.pictureSize.height +
", expected = " + expConfig.pictureSize.width + "x" + expConfig.pictureSize.height);
ok(cfg.recorderProfile === expConfig.recorderProfile,
"Configured recorder profile = '" + cfg.recorderProfile + "'" +
", expected = '" + expConfig.recorderProfile + "'");
}
function setAndVerifyConfig(setConfig, expConfig)
{
return suite.getCamera(undefined, setConfig)
.catch(suite.rejectGetCamera)
.then(function(p) {
verifyConfig(p.configuration, expConfig);
});
}
suite.test('bad-initial-config', function() {
function getCamera() {
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var config = {
mode: 'picture',
recorderProfile: 'foobar',
};
return navigator.mozCameras.getCamera(whichCamera, config);
}
function rejectGetCamera(error) {
ok(error.name === "NS_ERROR_NOT_AVAILABLE",
"getCamera() failed with: " + error.name);
return Promise.resolve();
}
return getCamera()
.then(suite.expectedRejectGetCamera, rejectGetCamera);
});
suite.test('start-unspecified', function() {
// bug 1037322
var cameraManager = navigator.mozCameras;
var whichCamera = cameraManager.getListOfCameras()[0];
var postConfig = {
mode: 'picture',
recorderProfile: 'low',
previewSize: {
width: 320,
height: 240
},
pictureSize: {
width: 320,
height: 240
}
};
function resolveGetCamera(p) {
suite.camera = p.camera;
// Check the default configuration
var cfg = p.configuration;
ok(cfg.mode === "unspecified", "Initial mode = " + cfg.mode);
ok(cfg.previewSize.width === 0 && cfg.previewSize.height === 0,
"Initial preview size = " + cfg.previewSize.width + "x" + cfg.previewSize.height);
ok(cfg.recorderProfile === "default",
"Initial recorder profile = '" + cfg.recorderProfile + "'");
}
function configure(p) {
// Apply our specific configuration
return suite.camera.setConfiguration(postConfig);
}
function resolveConfigure(cfg) {
// Check our specific configuration
verifyConfig(cfg, postConfig);
}
return cameraManager.getCamera(whichCamera, {mode: 'unspecified'})
.then(resolveGetCamera, suite.rejectGetCamera)
.then(configure)
.then(resolveConfigure, suite.rejectConfigure);
});
suite.test('picture-mode', function() {
suite.hw.params['preview-size'] = '1x1';
suite.hw.params['picture-size'] = '1x1';
suite.hw.params['preview-size-values'] = '640x480,320x240,1x1';
suite.hw.params['picture-size-values'] = '640x480,320x240,1x1';
suite.hw.params['video-size-values'] = '320x240';
var setConfig = {
mode: 'picture',
previewSize: {
width: 640,
height: 480
}
};
var expConfig = {
mode: 'picture',
recorderProfile: 'qvga',
previewSize: {
width: 640,
height: 480
},
pictureSize: {
width: 640,
height: 480
}
};
return setAndVerifyConfig(setConfig, expConfig);
});
suite.test('picture-mode-larger-picture-size', function() {
suite.hw.params['preview-size'] = '1x1';
suite.hw.params['picture-size'] = '1x1';
suite.hw.params['preview-size-values'] = '640x480,320x240,1x1';
suite.hw.params['picture-size-values'] = '1280x960,640x480,320x240,1x1';
suite.hw.params['video-size-values'] = '320x240';
var setConfig = {
mode: 'picture',
previewSize: {
width: 640,
height: 480
}
};
var expConfig = {
mode: 'picture',
recorderProfile: 'qvga',
previewSize: {
width: 640,
height: 480
},
pictureSize: {
width: 1280,
height: 960
}
};
return setAndVerifyConfig(setConfig, expConfig);
});
suite.test('picture-mode-size-unsupported-big', function() {
suite.hw.params['preview-size'] = '1x1';
suite.hw.params['picture-size'] = '1x1';
suite.hw.params['preview-size-values'] = '640x480,320x240,1x1';
suite.hw.params['picture-size-values'] = '1280x960,640x480,320x240,1x1';
suite.hw.params['video-size-values'] = '320x240';
var setConfig = {
mode: 'picture',
previewSize: {
width: 640,
height: 480
},
pictureSize: {
width: 2000,
height: 2000
},
};
var expConfig = {
mode: 'picture',
recorderProfile: 'qvga',
previewSize: {
width: 640,
height: 480
},
pictureSize: {
width: 1280,
height: 960
},
};
return setAndVerifyConfig(setConfig, expConfig);
});
suite.test('picture-mode-size-unsupported', function() {
suite.hw.params['preview-size'] = '1x1';
suite.hw.params['picture-size'] = '1x1';
suite.hw.params['preview-size-values'] = '640x480,320x240,1x1';
suite.hw.params['picture-size-values'] = '1280x960,640x480,320x240,100x100,1x1';
suite.hw.params['video-size-values'] = '320x240';
var setConfig = {
mode: 'picture',
previewSize: {
width: 640,
height: 480
},
pictureSize: {
width: 641,
height: 481,
},
};
var expConfig = {
mode: 'picture',
recorderProfile: 'qvga',
previewSize: {
width: 640,
height: 480
},
pictureSize: {
width: 640,
height: 480
},
};
return setAndVerifyConfig(setConfig, expConfig);
});
suite.test('picture-mode-asr-mismatch', function() {
suite.hw.params['preview-size'] = '320x240';
suite.hw.params['picture-size'] = '640x480';
suite.hw.params['preview-size-values'] = '640x480,320x240,50x50';
suite.hw.params['picture-size-values'] = '1280x960,640x480,320x240,100x100';
suite.hw.params['video-size-values'] = '320x240';
var setConfig = {
mode: 'picture',
previewSize: {
width: 640,
height: 480
},
pictureSize: {
width: 100,
height: 100,
},
};
var expConfig = {
mode: 'picture',
recorderProfile: 'qvga',
previewSize: {
width: 50,
height: 50
},
pictureSize: {
width: 100,
height: 100,
},
};
return setAndVerifyConfig(setConfig, expConfig);
});
suite.test('picture-mode-update-video-size', function() {
suite.hw.params['preview-size'] = '320x240';
suite.hw.params['picture-size'] = '640x480';
suite.hw.params['video-size'] = '50x50';
suite.hw.params['preview-size-values'] = '640x480,320x240,50x50';
suite.hw.params['video-size-values'] = '1280x960,640x480,320x240,50x50';
suite.hw.params['picture-size-values'] = '1280x960,640x480,320x240,100x100';
var setConfig = {
mode: 'picture',
recorderProfile: 'qvga',
previewSize: {
width: 320,
height: 240
},
pictureSize: {
width: 320,
height: 240,
},
};
var expConfig = {
mode: 'picture',
recorderProfile: 'qvga',
previewSize: {
width: 320,
height: 240
},
pictureSize: {
width: 320,
height: 240,
},
};
function checkVideoSize(p) {
ok(suite.hw.params['video-size'] === '320x240', 'video size reset with picture mode switch');
}
return setAndVerifyConfig(setConfig, expConfig)
.then(checkVideoSize);
});
suite.test('picture-mode-no-update-video-size', function() {
suite.hw.params['preview-size'] = '320x240';
suite.hw.params['picture-size'] = '640x480';
suite.hw.params['video-size'] = '1280x960';
suite.hw.params['preview-size-values'] = '640x480,320x240,50x50';
suite.hw.params['video-size-values'] = '1280x960,640x480,320x240,50x50';
suite.hw.params['picture-size-values'] = '1280x960,640x480,320x240,100x100';
var setConfig = {
mode: 'picture',
recorderProfile: 'qvga',
previewSize: {
width: 640,
height: 480
},
pictureSize: {
width: 640,
height: 480,
},
};
var expConfig = {
mode: 'picture',
recorderProfile: 'qvga',
previewSize: {
width: 640,
height: 480
},
pictureSize: {
width: 640,
height: 480,
},
};
function checkVideoSize(p) {
ok(suite.hw.params['video-size'] === '1280x960', 'video size retained with picture mode switch');
}
return setAndVerifyConfig(setConfig, expConfig)
.then(checkVideoSize);
});
suite.test('video-mode-preview-size', function() {
suite.hw.params['preview-size'] = '1x1';
suite.hw.params['picture-size'] = '1x1';
suite.hw.params['recording-hint'] = 'false';
suite.hw.params['preview-size-values'] = '640x480,320x240,1x1';
suite.hw.params['picture-size-values'] = '700x700,640x480,320x240,1x1';
var setConfig = {
mode: 'video',
recorderProfile: 'qvga'
};
var expConfig = {
mode: 'video',
recorderProfile: 'qvga',
previewSize: {
width: 320,
height: 240
},
pictureSize: {
width: 700,
height: 700
}
};
function checkRecordingHint(p) {
ok(suite.hw.params['recording-hint'] === 'true', 'recording hint enabled');
}
return setAndVerifyConfig(setConfig, expConfig)
.then(checkRecordingHint);
});
suite.test('video-mode', function() {
suite.hw.params['preview-size'] = '1x1';
suite.hw.params['picture-size'] = '1x1';
suite.hw.params['video-size'] = '1x1';
suite.hw.params['recording-hint'] = 'false';
suite.hw.params['preferred-preview-size-for-video'] = '640x480';
suite.hw.params['preview-size-values'] = '640x480,320x240,1x1';
suite.hw.params['picture-size-values'] = '700x700,640x480,320x240,1x1';
suite.hw.params['video-size-values'] = '700x700,640x480,320x240,1x1';
var setConfig = {
mode: 'video',
recorderProfile: 'qvga',
previewSize: {
width: 320,
height: 240
}
};
var expConfig = {
mode: 'video',
recorderProfile: 'qvga',
previewSize: {
width: 320,
height: 240
},
pictureSize: {
width: 700,
height: 700
}
};
function checkRecordingHint(p) {
ok(suite.hw.params['recording-hint'] === 'true', 'recording hint enabled');
}
return setAndVerifyConfig(setConfig, expConfig)
.then(checkRecordingHint);
});
suite.test('video-mode-larger-preview-size', function() {
suite.hw.params['preview-size'] = '1x1';
suite.hw.params['picture-size'] = '1x1';
suite.hw.params['video-size'] = '1x1';
suite.hw.params['recording-hint'] = 'false';
suite.hw.params['preferred-preview-size-for-video'] = '640x480';
suite.hw.params['preview-size-values'] = '640x480,320x240,1x1';
suite.hw.params['picture-size-values'] = '700x700,640x480,320x240,1x1';
suite.hw.params['video-size-values'] = '700x700,640x480,320x240,1x1';
var setConfig = {
mode: 'video',
recorderProfile: 'qvga',
previewSize: {
width: 640,
height: 480
}
};
var expConfig = {
mode: 'video',
recorderProfile: 'qvga',
previewSize: {
width: 320,
height: 240
},
pictureSize: {
width: 700,
height: 700
}
};
return setAndVerifyConfig(setConfig, expConfig);
});
suite.test('video-mode-smaller-preview-size', function() {
suite.hw.params['preview-size'] = '1x1';
suite.hw.params['picture-size'] = '1x1';
suite.hw.params['video-size'] = '1x1';
suite.hw.params['recording-hint'] = 'false';
suite.hw.params['preferred-preview-size-for-video'] = '640x480';
suite.hw.params['preview-size-values'] = '640x480,320x240,160x120,1x1';
suite.hw.params['picture-size-values'] = '700x700,640x480,320x240,1x1';
suite.hw.params['video-size-values'] = '700x700,640x480,320x240,1x1';
var setConfig = {
mode: 'video',
recorderProfile: 'qvga',
previewSize: {
width: 160,
height: 120
}
};
var expConfig = {
mode: 'video',
recorderProfile: 'qvga',
previewSize: {
width: 160,
height: 120
},
pictureSize: {
width: 700,
height: 700
}
};
return setAndVerifyConfig(setConfig, expConfig);
});
suite.test('video-mode-larger-preview-size-than-preferred', function() {
suite.hw.params['preview-size'] = '1x1';
suite.hw.params['picture-size'] = '1x1';
suite.hw.params['video-size'] = '1x1';
suite.hw.params['recording-hint'] = 'false';
suite.hw.params['preferred-preview-size-for-video'] = '200x200';
suite.hw.params['preview-size-values'] = '640x480,320x240,160x120,1x1';
suite.hw.params['picture-size-values'] = '700x700,640x480,320x240,1x1';
suite.hw.params['video-size-values'] = '700x700,640x480,400x400,320x240,1x1';
var setConfig = {
mode: 'video',
recorderProfile: 'qvga',
previewSize: {
width: 320,
height: 240
}
};
var expConfig = {
mode: 'video',
recorderProfile: 'qvga',
previewSize: {
width: 160,
height: 120
},
pictureSize: {
width: 700,
height: 700
}
};
return setAndVerifyConfig(setConfig, expConfig);
});
suite.setup()
.then(suite.run);
</script>
</body>
</html>
@@ -92,8 +92,93 @@ function compareFace(aFace, expected)
var suite = new CameraTestSuite();
suite.test('face-detection-op', function() {
function start(p) {
return new Promise(function(resolve, reject) {
suite.hw.attach({
startFaceDetection: function() {
ok(true, "startFaceDetection() requested");
resolve();
}
});
try {
suite.camera.startFaceDetection();
ok(true, "startFaceDetection() succeeded");
} catch(e) {
ok(false, "startFaceDetection() failed with: " + e.name);
resolve();
}
});
}
function stop(p) {
return new Promise(function(resolve, reject) {
suite.hw.attach({
stopFaceDetection: function() {
ok(true, "stopFaceDetection() requested");
resolve();
}
});
try {
suite.camera.stopFaceDetection();
ok(true, "stopFaceDetection() succeeded");
} catch(e) {
ok(false, "stopFaceDetection() failed with: " + e.name);
resolve();
}
});
}
function startFailure(p) {
return new Promise(function(resolve, reject) {
suite.hw.attach({
startFaceDetection: function() {
ok(true, "startFaceDetection() requested and failed");
resolve();
throw SpecialPowers.Cr.NS_ERROR_FAILURE;
}
});
try {
suite.camera.startFaceDetection();
ok(true, "startFaceDetection() succeeded and error swallowed");
} catch(e) {
ok(false, "startFaceDetection() failed with: " + e.name);
resolve();
}
});
}
function stopFailure(p) {
return new Promise(function(resolve, reject) {
suite.hw.attach({
stopFaceDetection: function() {
ok(true, "stopFaceDetection() requested and failed");
resolve();
throw SpecialPowers.Cr.NS_ERROR_FAILURE;
}
});
try {
suite.camera.stopFaceDetection();
ok(true, "stopFaceDetection() succeeded and error swallowed");
} catch(e) {
ok(false, "stopFaceDetection() failed with: " + e.name);
resolve();
}
});
}
return suite.getCamera()
.then(start, suite.rejectGetCamera)
.then(stop)
.then(startFailure)
.then(stopFailure);
});
suite.test('face-detection', function() {
function detectFace(msg, expected) {
function detectFace(msg, given, expected) {
if (expected === undefined) {
expected = given;
}
var sync = new Promise(function(resolve, reject) {
function onEvent(evt) {
try {
@@ -108,7 +193,7 @@ suite.test('face-detection', function() {
suite.camera.addEventListener('facesdetected', onEvent);
});
suite.hw.fireFacesDetected(expected);
suite.hw.fireFacesDetected(given);
return sync;
}
@@ -220,11 +305,47 @@ suite.test('face-detection', function() {
);
}
function detectOneFaceExcessScore() {
return detectFace('one-face-excess-score',
{
faces: [ {
id: 1,
score: 120,
bounds: {
left: 3,
top: 4,
right: 5,
bottom: 6
},
leftEye: null,
rightEye: null,
mouth: null
} ]
},
{
faces: [ {
id: 1,
score: 100,
bounds: {
left: 3,
top: 4,
right: 5,
bottom: 6
},
leftEye: null,
rightEye: null,
mouth: null
} ]
}
);
}
return suite.getCamera()
.then(detectOneFace, suite.rejectGetCamera)
.then(detectTwoFaces)
.then(detectOneFaceNoFeatures)
.then(detectNoFaces);
.then(detectNoFaces)
.then(detectOneFaceExcessScore);
});
suite.setup()
@@ -132,6 +132,133 @@ suite.test('fake-low-memory-platform', function() {
.then(resolve, suite.rejectGetCamera);
});
suite.test('fake-effects', function() {
var supportedValues = ['none', 'mono', 'negative', 'solarize', 'sepia', 'posterize', 'whiteboard', 'blackboard', 'aqua'];
suite.hw.params['effect'] = 'none';
suite.hw.params['effect-values'] = supportedValues.join(',');
function resolve(p) {
var cap = suite.camera.capabilities;
ok(cap.effects.length == supportedValues.length, "Effects length = " + cap.effects.length);
// make sure expected values are present
supportedValues.forEach(function(val) {
ok(cap.effects.indexOf(val) != -1, "Effect '" + val + "' is present");
});
}
return suite.getCamera()
.then(resolve, suite.rejectGetCamera);
});
suite.test('fake-flash-modes', function() {
var supportedValues = ['off', 'auto', 'on', 'red-eye', 'torch'];
suite.hw.params['flash-mode'] = 'auto';
suite.hw.params['flash-mode-values'] = supportedValues.join(',');
function resolve(p) {
var cam = suite.camera;
var cap = cam.capabilities;
ok(cap.flashModes.length == supportedValues.length, "Flash modes length = " + cap.flashModes.length);
// make sure expected values are present
supportedValues.forEach(function(val) {
ok(cap.flashModes.indexOf(val) != -1, "Flash mode '" + val + "' is present");
});
// test setters/getters
cap.flashModes.forEach(function(val, index) {
cam.flashMode = val;
ok(cam.flashMode === val,
"Flash Mode [" + index + "] = " + val + ", cam.flashMode = " + cam.flashMode);
});
}
return suite.getCamera()
.then(resolve, suite.rejectGetCamera);
});
suite.test('fake-focus-modes', function() {
var supportedValues = ['auto', 'infinity', 'macro', 'fixed', 'edof', 'continuous-video'];
suite.hw.params['focus-mode'] = 'auto';
suite.hw.params['focus-mode-values'] = supportedValues.join(',');
function resolve(p) {
var cam = suite.camera;
var cap = cam.capabilities;
ok(cap.focusModes.length == supportedValues.length, "Focus modes length = " + cap.focusModes.length);
// make sure expected values are present
supportedValues.forEach(function(val) {
ok(cap.focusModes.indexOf(val) != -1, "Focus mode '" + val + "' is present");
});
// test setters/getters
cap.focusModes.forEach(function(val, index) {
cam.focusMode = val;
ok(cam.focusMode === val,
"Focus Mode [" + index + "] = " + val + ", cam.focusMode = " + cam.focusMode);
});
}
return suite.getCamera()
.then(resolve, suite.rejectGetCamera);
});
suite.test('fake-white-balance-modes', function() {
var supportedValues = ['auto', 'incandescent', 'fluorescent', 'warm-fluorescent', 'daylight', 'cloudy-daylight', 'twilight', 'shade'];
suite.hw.params['whitebalance'] = 'auto';
suite.hw.params['whitebalance-values'] = supportedValues.join(',');
function resolve(p) {
var cam = suite.camera;
var cap = cam.capabilities;
ok(cap.whiteBalanceModes.length == supportedValues.length, "White balance modes length = " + cap.whiteBalanceModes.length);
// make sure expected values are present
supportedValues.forEach(function(val) {
ok(cap.whiteBalanceModes.indexOf(val) != -1, "White balance mode '" + val + "' is present");
});
// test setters/getters
cap.whiteBalanceModes.forEach(function(val, index) {
cam.whiteBalanceMode = val;
ok(cam.whiteBalanceMode === val,
"White balance mode [" + index + "] = " + val + ", cam.whiteBalanceMode = " + cam.whiteBalanceMode);
});
}
return suite.getCamera()
.then(resolve, suite.rejectGetCamera);
});
suite.test('fake-video-sizes', function() {
var supportedValues = ['auto', 'incandescent', 'fluorescent', 'warm-fluorescent', 'daylight', 'cloudy-daylight', 'twilight', 'shade'];
suite.hw.params['whitebalance'] = 'auto';
suite.hw.params['whitebalance-values'] = supportedValues.join(',');
function resolve(p) {
var cam = suite.camera;
var cap = cam.capabilities;
ok(cap.whiteBalanceModes.length == supportedValues.length, "White balance modes length = " + cap.whiteBalanceModes.length);
// make sure expected values are present
supportedValues.forEach(function(val) {
ok(cap.whiteBalanceModes.indexOf(val) != -1, "White balance mode '" + val + "' is present");
});
// test setters/getters
cap.whiteBalanceModes.forEach(function(val, index) {
cam.whiteBalanceMode = val;
ok(cam.whiteBalanceMode === val,
"White balance mode [" + index + "] = " + val + ", cam.whiteBalanceMode = " + cam.whiteBalanceMode);
});
}
return suite.getCamera()
.then(resolve, suite.rejectGetCamera);
});
suite.test('fake-iso', function() {
suite.hw.params['iso'] = 'auto';
suite.hw.params['iso-values'] = 'auto,ISO_HJR,ISO100,foo,ISObar,ISO150moz,ISO200,400,ISO800,1600';
@@ -158,9 +285,9 @@ suite.test('fake-iso', function() {
// test setters/getters for individual ISO modes
cap.isoModes.forEach(function(iso, index) {
cam.iso = iso;
ok(cam.iso === iso,
"ISO[" + index + "] = " + iso + ", cam.iso = " + cam.iso);
cam.isoMode = iso;
ok(cam.isoMode === iso,
"ISO[" + index + "] = " + iso + ", cam.iso = " + cam.isoMode);
});
}
@@ -168,6 +295,19 @@ suite.test('fake-iso', function() {
.then(resolve, suite.rejectGetCamera);
});
suite.test('fake-faces-detected', function() {
suite.hw.params['max-num-detected-faces-hw'] = '5';
function resolve(p) {
var cap = suite.camera.capabilities;
ok(cap.maxDetectedFaces == 5, "maxDetectedFaces = " + cap.maxDetectedFaces);
}
return suite.getCamera()
.then(resolve, suite.rejectGetCamera);
});
suite.test('fake-metering-areas', function() {
suite.hw.params['max-num-metering-areas'] = '1';
+162
View File
@@ -0,0 +1,162 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test Camera Recording</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="camera_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<video id="viewfinder" width = "200" height = "200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<script class="testbody" type="text/javascript;version=1.7">
var suite = new CameraTestSuite();
var baseConfig = {
mode: 'video',
};
var testFilePath = 'test.3gp';
var storage = navigator.getDeviceStorage("videos");
function cleanup()
{
return storage.delete(testFilePath).then(function(p) {
}, function(e) {
Promise.resolve();
});
}
suite.test('recording', function() {
function startRecording(p) {
var eventPromise = new Promise(function(resolve, reject) {
function onEvent(evt) {
ok(evt.newState === 'Started', 'recorder state change event = ' + evt.newState);
suite.camera.removeEventListener('recorderstatechange', onEvent);
resolve();
}
suite.camera.addEventListener('recorderstatechange', onEvent);
});
var domPromise = suite.camera.startRecording({}, storage, testFilePath);
return Promise.all([domPromise, eventPromise]);
}
function stopRecording(p) {
var eventPromise = new Promise(function(resolve, reject) {
function onEvent(evt) {
ok(evt.newState === 'Stopped', 'recorder state change event = ' + evt.newState);
suite.camera.removeEventListener('recorderstatechange', onEvent);
resolve();
}
suite.camera.addEventListener('recorderstatechange', onEvent);
});
var domPromise = new Promise(function(resolve, reject) {
try {
suite.camera.stopRecording();
resolve();
} catch(e) {
reject(e);
}
});
return Promise.all([domPromise, eventPromise]);
}
return suite.getCamera(undefined, baseConfig)
.then(cleanup, suite.rejectGetCamera)
.then(startRecording)
.then(stopRecording, suite.rejectStartRecording)
.catch(suite.rejectStopRecording);
});
// bug 1152500
suite.test('interrupt-record', function() {
function startRecording(p) {
var startPromise = suite.camera.startRecording({}, storage, testFilePath);
suite.camera.stopRecording();
return startPromise;
}
function rejectStartRecording(e) {
ok(e.name === 'NS_ERROR_ABORT', 'onError called correctly on startRecording interrupted: ' + e);
}
return suite.getCamera(undefined, baseConfig)
.then(cleanup, suite.rejectGetCamera)
.then(startRecording)
.then(suite.expectedRejectStartRecording, rejectStartRecording);
});
// bug 1152500
suite.test('already-initiated-recording', function() {
function startRecording(p) {
return new Promise(function(resolve, reject) {
var firstCall = false;
var secondCall = false;
function end() {
if (firstCall && secondCall) {
resolve();
}
}
suite.camera.startRecording({}, storage, testFilePath).then(function(p) {
ok(true, "First call to startRecording() succeeded");
firstCall = true;
end();
}, function(e) {
ok(false, "First call to startRecording() failed unexpectedly with: " + e);
firstCall = true;
end();
});
suite.camera.startRecording({}, storage, testFilePath).then(function(p) {
ok(false, "Second call to startRecording() succeeded unexpectedly");
secondCall = true;
end();
}, function(e) {
ok(e.name === 'NS_ERROR_IN_PROGRESS', "Second call to startRecording() failed expectedly with: " + e);
secondCall = true;
end();
});
});
}
return suite.getCamera(undefined, baseConfig)
.then(cleanup, suite.rejectGetCamera)
.then(startRecording);
});
// bug 1152500
suite.test('already-started-recording', function() {
function startRecording(p) {
return suite.camera.startRecording({}, storage, testFilePath);
}
function startRecordingAgain(p) {
return suite.camera.startRecording({}, storage, testFilePath);
}
function rejectStartRecordingAgain(e) {
ok(e.name === 'NS_ERROR_IN_PROGRESS', "Second call to startRecording() failed expectedly with: " + e);
}
return suite.getCamera(undefined, baseConfig)
.then(cleanup, suite.rejectGetCamera)
.then(startRecording)
.then(startRecordingAgain, suite.rejectStartRecording)
.then(suite.expectedRejectStartRecording, rejectStartRecordingAgain)
});
suite.setup()
.then(suite.run);
</script>
</body>
</html>
@@ -18,6 +18,39 @@ function cameraRelease(p) {
return suite.camera.release();
}
suite.test('release-close-event', function() {
// bug 1099390
function release(p) {
return new Promise(function(resolve, reject) {
var gotCloseEvent = false;
var gotReleasePromise = false;
var onClosed = function(e) {
suite.camera.removeEventListener('close', onClosed);
ok(!gotCloseEvent, "gotCloseEvent was " + gotCloseEvent);
ok(e.reason === "HardwareReleased", "'close' event reason is: " + e.reason);
gotCloseEvent = true;
if (gotReleasePromise) {
resolve();
}
};
suite.camera.addEventListener('close', onClosed);
suite.camera.release().then(function(p) {
ok(true, "released camera");
gotReleasePromise = true;
if (gotCloseEvent) {
resolve();
}
}).catch(reject);
});
}
return suite.getCamera()
.then(release, suite.rejectGetCamera);
});
suite.test('release-after-release', function() {
return suite.getCamera()
.then(cameraRelease, suite.rejectGetCamera)
@@ -157,6 +190,33 @@ suite.test('stop-recording-after-release', function() {
.then(stopRecording, suite.rejectRelease);
});
suite.test('face-detection-after-release', function() {
function startFaceDetection(p) {
try {
suite.camera.startFaceDetection();
ok(false, "startFaceDetection() should have failed");
} catch(e) {
ok(e.result === SpecialPowers.Cr.NS_ERROR_NOT_AVAILABLE,
"startFaceDetection() failed with: " + e.name);
}
}
function stopFaceDetection(p) {
try {
suite.camera.stopFaceDetection();
ok(false, "stopFaceDetection() should have failed");
} catch(e) {
ok(e.result === SpecialPowers.Cr.NS_ERROR_NOT_AVAILABLE,
"stopFaceDetection() failed with: " + e.name);
}
}
return suite.getCamera()
.then(cameraRelease, suite.rejectGetCamera)
.then(startFaceDetection, suite.rejectRelease)
.then(stopFaceDetection);
});
suite.test('set-configuration-after-release', function() {
function configure(p) {
return suite.camera.setConfiguration(null);
@@ -0,0 +1,165 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=940424
-->
<head>
<title>Bug 940424 - Test camera hardware API failure handling</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="camera_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=940424">Mozilla Bug 940424</a>
<video id="viewfinder" width = "200" height = "200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<script class="testbody" type="text/javascript;version=1.7">
var suite = new CameraTestSuite();
suite.test('take-picture-failures', function() {
function startTakePictureProcessError(p) {
suite.hw.attach({
takePicture: function() {
suite.hw.fireTakePictureError();
}
});
return suite.camera.takePicture();
}
function rejectTakePictureProcessError(e) {
ok(e.name === 'NS_ERROR_FAILURE', 'takePicture() process should fail: ' + e);
}
function startTakePictureError(p) {
suite.hw.attach({
takePicture: function() {
throw SpecialPowers.Cr.NS_ERROR_FAILURE;
}
});
return suite.camera.takePicture();
}
function rejectTakePictureError(e) {
ok(e.name === 'NS_ERROR_FAILURE', 'takePicture() should fail: ' + e);
}
return suite.getCamera()
.catch(suite.rejectGetCamera)
.then(startTakePictureProcessError)
.then(suite.expectedRejectTakePicture, rejectTakePictureProcessError)
.then(startTakePictureError)
.then(suite.expectedRejectTakePicture, rejectTakePictureError)
});
suite.test('shutter', function() {
function shutter(p) {
return new Promise(function(resolve, reject) {
function onShutter(e) {
ok(true, 'received shutter event');
suite.camera.removeEventListener('shutter', onShutter);
resolve();
}
suite.camera.addEventListener('shutter', onShutter);
suite.hw.fireShutter();
});
}
return suite.getCamera()
.then(shutter, suite.rejectGetCamera)
});
suite.test('take-picture', function() {
suite.hw.params['picture-format-values'] = 'jpeg,png';
suite.hw.params['picture-format'] = 'jpeg';
var config = {
fileFormat: 'png',
latitude: 1.0,
longitude: 2.0,
altitude: 3.0,
timestamp: 4
};
var data = 'this is a test';
var eventPromise;
function takePicture(p) {
eventPromise = new Promise(function(resolve, reject) {
function onPicture(evt) {
ok(true, 'got picture event');
try {
verifyPicture(evt.data);
} catch(e) {
reject(e);
}
suite.camera.removeEventListener('picture', onPicture);
resolve();
}
suite.camera.addEventListener('picture', onPicture);
});
suite.hw.attach({
takePicture: function() {
ok(suite.hw.params['picture-format'] === 'png', "requested format is '" + suite.hw.params['picture-format'] + "'");
suite.hw.fireTakePictureComplete(new window.Blob([data], {'type': config.fileFormat}));
}
});
return suite.camera.takePicture(config);
}
function verifyPicture(blob) {
ok(blob.size == data.length, "picture blob is " + blob.size + " bytes");
ok(blob.type === 'image/' + config.fileFormat, "picture blob format is '" + blob.type + "'");
}
function tookPicture(p) {
ok(true, 'got picture promise');
verifyPicture(p);
return eventPromise;
}
return suite.getCamera()
.then(takePicture, suite.rejectGetCamera)
.then(tookPicture, suite.rejectTakePicture);
});
suite.test('take-picture-no-config', function() {
var data = 'this is a test';
var format = 'jpeg';
suite.hw.params['picture-format-values'] = 'jpeg,png';
suite.hw.params['picture-format'] = format;
function takePicture(p) {
suite.hw.attach({
takePicture: function() {
ok(suite.hw.params['picture-format'] === format, "requested format is '" + suite.hw.params['picture-format'] + "'");
suite.hw.fireTakePictureComplete(new window.Blob([data], {'type': format}));
}
});
return suite.camera.takePicture();
}
function verifyPicture(blob) {
ok(blob.size == data.length, "picture blob is " + blob.size + " bytes");
ok(blob.type === 'image/' + format, "picture blob format is '" + blob.type + "'");
}
return suite.getCamera()
.then(takePicture, suite.rejectGetCamera)
.then(verifyPicture, suite.rejectTakePicture);
});
suite.setup()
.then(suite.run);
</script>
</body>
</html>
+10 -30
View File
@@ -7,7 +7,6 @@
#ifndef DeviceStorage_h
#define DeviceStorage_h
#include "nsIDOMDeviceStorage.h"
#include "nsIFile.h"
#include "nsIPrincipal.h"
#include "nsIObserver.h"
@@ -25,6 +24,7 @@
class nsIInputStream;
class nsIOutputStream;
struct DeviceStorageFileDescriptor;
namespace mozilla {
class EventListenerManager;
@@ -159,7 +159,6 @@ class FileUpdateDispatcher final
class nsDOMDeviceStorage final
: public mozilla::DOMEventTargetHelper
, public nsIDOMDeviceStorage
, public nsIObserver
{
typedef mozilla::ErrorResult ErrorResult;
@@ -173,27 +172,12 @@ public:
typedef nsTArray<nsString> VolumeNameArray;
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_NSIDOMDEVICESTORAGE
NS_DECL_NSIOBSERVER
NS_DECL_NSIDOMEVENTTARGET
NS_REALLY_FORWARD_NSIDOMEVENTTARGET(DOMEventTargetHelper)
virtual mozilla::EventListenerManager*
GetExistingListenerManager() const override;
virtual mozilla::EventListenerManager*
GetOrCreateListenerManager() override;
virtual void
AddEventListener(const nsAString& aType,
mozilla::dom::EventListener* aListener,
bool aUseCapture,
const mozilla::dom::Nullable<bool>& aWantsUntrusted,
ErrorResult& aRv) override;
virtual void RemoveEventListener(const nsAString& aType,
mozilla::dom::EventListener* aListener,
bool aUseCapture,
ErrorResult& aRv) override;
void EventListenerWasAdded(const nsAString& aType,
ErrorResult& aRv,
JSCompartment* aCompartment) override;
explicit nsDOMDeviceStorage(nsPIDOMWindow* aWindow);
@@ -273,13 +257,16 @@ public:
already_AddRefed<DOMRequest> Mount(ErrorResult& aRv);
already_AddRefed<DOMRequest> Unmount(ErrorResult& aRv);
already_AddRefed<DOMRequest> CreateFileDescriptor(const nsAString& aPath,
DeviceStorageFileDescriptor* aDSFD,
ErrorResult& aRv);
bool CanBeMounted();
bool CanBeFormatted();
bool CanBeShared();
bool IsRemovable();
bool Default();
// Uses XPCOM GetStorageName
void GetStorageName(nsAString& aStorageName);
already_AddRefed<Promise>
GetRoot(ErrorResult& aRv);
@@ -366,13 +353,6 @@ private:
void DispatchStorageStatusChangeEvent(nsAString& aStorageStatus);
#endif
// nsIDOMDeviceStorage.type
enum {
DEVICE_STORAGE_TYPE_DEFAULT = 0,
DEVICE_STORAGE_TYPE_SHARED,
DEVICE_STORAGE_TYPE_EXTERNAL
};
nsRefPtr<DeviceStorageFileSystem> mFileSystem;
};
+26 -270
View File
@@ -3360,8 +3360,7 @@ NS_IMPL_CYCLE_COLLECTION(DeviceStorageRequest,
mDeviceStorage)
NS_INTERFACE_MAP_BEGIN(nsDOMDeviceStorage)
NS_INTERFACE_MAP_ENTRY(nsIDOMDeviceStorage)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(nsDOMDeviceStorage)
NS_INTERFACE_MAP_ENTRY(nsIObserver)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
@@ -3741,15 +3740,6 @@ nsDOMDeviceStorage::IsAvailable()
return dsf->IsAvailable();
}
NS_IMETHODIMP
nsDOMDeviceStorage::Add(nsIDOMBlob *aBlob, nsIDOMDOMRequest * *_retval)
{
ErrorResult rv;
nsRefPtr<DOMRequest> request = Add(static_cast<Blob*>(aBlob), rv);
request.forget(_retval);
return rv.StealNSResult();
}
already_AddRefed<DOMRequest>
nsDOMDeviceStorage::Add(Blob* aBlob, ErrorResult& aRv)
{
@@ -3788,17 +3778,6 @@ nsDOMDeviceStorage::Add(Blob* aBlob, ErrorResult& aRv)
return AddNamed(aBlob, NS_ConvertASCIItoUTF16(path), aRv);
}
NS_IMETHODIMP
nsDOMDeviceStorage::AddNamed(nsIDOMBlob *aBlob,
const nsAString & aPath,
nsIDOMDOMRequest * *_retval)
{
ErrorResult rv;
nsRefPtr<DOMRequest> request = AddNamed(static_cast<Blob*>(aBlob), aPath, rv);
request.forget(_retval);
return rv.StealNSResult();
}
already_AddRefed<DOMRequest>
nsDOMDeviceStorage::AddNamed(Blob* aBlob, const nsAString& aPath,
ErrorResult& aRv)
@@ -3886,25 +3865,6 @@ nsDOMDeviceStorage::AddOrAppendNamed(Blob* aBlob, const nsAString& aPath,
return request.forget();
}
NS_IMETHODIMP
nsDOMDeviceStorage::Get(const nsAString& aPath, nsIDOMDOMRequest** aRetval)
{
ErrorResult rv;
nsRefPtr<DOMRequest> request = Get(aPath, rv);
request.forget(aRetval);
return rv.StealNSResult();
}
NS_IMETHODIMP
nsDOMDeviceStorage::GetEditable(const nsAString& aPath,
nsIDOMDOMRequest** aRetval)
{
ErrorResult rv;
nsRefPtr<DOMRequest> request = GetEditable(aPath, rv);
request.forget(aRetval);
return rv.StealNSResult();
}
already_AddRefed<DOMRequest>
nsDOMDeviceStorage::GetInternal(const nsAString& aPath, bool aEditable,
ErrorResult& aRv)
@@ -3963,15 +3923,6 @@ nsDOMDeviceStorage::GetInternal(nsPIDOMWindow *aWin,
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
NS_IMETHODIMP
nsDOMDeviceStorage::Delete(const nsAString& aPath, nsIDOMDOMRequest** aRetval)
{
ErrorResult rv;
nsRefPtr<DOMRequest> request = Delete(aPath, rv);
request.forget(aRetval);
return rv.StealNSResult();
}
already_AddRefed<DOMRequest>
nsDOMDeviceStorage::Delete(const nsAString& aPath, ErrorResult& aRv)
{
@@ -4025,15 +3976,6 @@ nsDOMDeviceStorage::DeleteInternal(nsPIDOMWindow *aWin,
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
NS_IMETHODIMP
nsDOMDeviceStorage::FreeSpace(nsIDOMDOMRequest** aRetval)
{
ErrorResult rv;
nsRefPtr<DOMRequest> request = FreeSpace(rv);
request.forget(aRetval);
return rv.StealNSResult();
}
already_AddRefed<DOMRequest>
nsDOMDeviceStorage::FreeSpace(ErrorResult& aRv)
{
@@ -4059,15 +4001,6 @@ nsDOMDeviceStorage::FreeSpace(ErrorResult& aRv)
return request.forget();
}
NS_IMETHODIMP
nsDOMDeviceStorage::UsedSpace(nsIDOMDOMRequest** aRetval)
{
ErrorResult rv;
nsRefPtr<DOMRequest> request = UsedSpace(rv);
request.forget(aRetval);
return rv.StealNSResult();
}
already_AddRefed<DOMRequest>
nsDOMDeviceStorage::UsedSpace(ErrorResult& aRv)
{
@@ -4097,15 +4030,6 @@ nsDOMDeviceStorage::UsedSpace(ErrorResult& aRv)
return request.forget();
}
NS_IMETHODIMP
nsDOMDeviceStorage::Available(nsIDOMDOMRequest** aRetval)
{
ErrorResult rv;
nsRefPtr<DOMRequest> request = Available(rv);
request.forget(aRetval);
return rv.StealNSResult();
}
already_AddRefed<DOMRequest>
nsDOMDeviceStorage::Available(ErrorResult& aRv)
{
@@ -4231,27 +4155,28 @@ nsDOMDeviceStorage::Unmount(ErrorResult& aRv)
return request.forget();
}
NS_IMETHODIMP
already_AddRefed<DOMRequest>
nsDOMDeviceStorage::CreateFileDescriptor(const nsAString& aPath,
DeviceStorageFileDescriptor* aDSFileDescriptor,
nsIDOMDOMRequest** aRequest)
ErrorResult& aRv)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aDSFileDescriptor);
nsCOMPtr<nsPIDOMWindow> win = GetOwner();
if (!win) {
return NS_ERROR_UNEXPECTED;
aRv.Throw(NS_ERROR_UNEXPECTED);
return nullptr;
}
DeviceStorageTypeChecker* typeChecker
= DeviceStorageTypeChecker::CreateOrGet();
if (!typeChecker) {
return NS_ERROR_FAILURE;
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
nsCOMPtr<nsIRunnable> r;
nsresult rv;
if (IsFullPath(aPath)) {
nsString storagePath;
@@ -4259,14 +4184,13 @@ nsDOMDeviceStorage::CreateFileDescriptor(const nsAString& aPath,
if (!ds) {
nsRefPtr<DOMRequest> request = new DOMRequest(win);
r = new PostErrorEvent(request, POST_ERROR_EVENT_UNKNOWN);
rv = NS_DispatchToCurrentThread(r);
if (NS_FAILED(rv)) {
return rv;
aRv = NS_DispatchToCurrentThread(r);
if (aRv.Failed()) {
return nullptr;
}
request.forget(aRequest);
return NS_OK;
return request.forget();
}
return ds->CreateFileDescriptor(storagePath, aDSFileDescriptor, aRequest);
return ds->CreateFileDescriptor(storagePath, aDSFileDescriptor, aRv);
}
nsRefPtr<DOMRequest> request = new DOMRequest(win);
@@ -4284,12 +4208,11 @@ nsDOMDeviceStorage::CreateFileDescriptor(const nsAString& aPath,
aDSFileDescriptor);
}
rv = NS_DispatchToCurrentThread(r);
if (NS_FAILED(rv)) {
return rv;
aRv = NS_DispatchToCurrentThread(r);
if (aRv.Failed()) {
return nullptr;
}
request.forget(aRequest);
return NS_OK;
return request.forget();
}
bool
@@ -4336,18 +4259,10 @@ nsDOMDeviceStorage::GetRoot(ErrorResult& aRv)
return mozilla::dom::Directory::GetRoot(mFileSystem, aRv);
}
NS_IMETHODIMP
nsDOMDeviceStorage::GetDefault(bool* aDefault)
{
*aDefault = Default();
return NS_OK;
}
NS_IMETHODIMP
void
nsDOMDeviceStorage::GetStorageName(nsAString& aStorageName)
{
aStorageName = mStorageName;
return NS_OK;
}
already_AddRefed<DOMCursor>
@@ -4550,46 +4465,19 @@ nsDOMDeviceStorage::Notify(const char* aReason, DeviceStorageFile* aFile)
return NS_OK;
}
NS_IMETHODIMP
nsDOMDeviceStorage::AddEventListener(const nsAString & aType,
nsIDOMEventListener *aListener,
bool aUseCapture,
bool aWantsUntrusted,
uint8_t aArgc)
{
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsPIDOMWindow> win = GetOwner();
if (!win) {
return NS_ERROR_UNEXPECTED;
}
nsRefPtr<DOMRequest> request = new DOMRequest(win);
nsRefPtr<DeviceStorageFile> dsf = new DeviceStorageFile(mStorageType,
mStorageName);
nsCOMPtr<nsIRunnable> r
= new DeviceStorageRequest(DEVICE_STORAGE_REQUEST_WATCH,
win, mPrincipal, dsf, request, this);
nsresult rv = NS_DispatchToCurrentThread(r);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return DOMEventTargetHelper::AddEventListener(aType, aListener, aUseCapture,
aWantsUntrusted, aArgc);
}
void
nsDOMDeviceStorage::AddEventListener(const nsAString & aType,
EventListener *aListener,
bool aUseCapture,
const Nullable<bool>& aWantsUntrusted,
ErrorResult& aRv)
nsDOMDeviceStorage::EventListenerWasAdded(const nsAString& aType,
ErrorResult& aRv,
JSCompartment* aCompartment)
{
MOZ_ASSERT(NS_IsMainThread());
if (!aType.EqualsLiteral("change")) {
return;
}
nsCOMPtr<nsPIDOMWindow> win = GetOwner();
if (!win) {
if (NS_WARN_IF(!win)) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return;
}
@@ -4602,138 +4490,6 @@ nsDOMDeviceStorage::AddEventListener(const nsAString & aType,
win, mPrincipal, dsf, request, this);
nsresult rv = NS_DispatchToCurrentThread(r);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
DOMEventTargetHelper::AddEventListener(aType, aListener, aUseCapture,
aWantsUntrusted, aRv);
}
NS_IMETHODIMP
nsDOMDeviceStorage::AddSystemEventListener(const nsAString & aType,
nsIDOMEventListener *aListener,
bool aUseCapture,
bool aWantsUntrusted,
uint8_t aArgc)
{
if (!mIsWatchingFile) {
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
obs->AddObserver(this, kFileWatcherUpdate, false);
mIsWatchingFile = true;
}
return nsDOMDeviceStorage::AddEventListener(aType, aListener, aUseCapture,
aWantsUntrusted, aArgc);
}
NS_IMETHODIMP
nsDOMDeviceStorage::RemoveEventListener(const nsAString & aType,
nsIDOMEventListener *aListener,
bool aUseCapture)
{
DOMEventTargetHelper::RemoveEventListener(aType, aListener, false);
if (mIsWatchingFile && !HasListenersFor(nsGkAtoms::onchange)) {
mIsWatchingFile = false;
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
obs->RemoveObserver(this, kFileWatcherUpdate);
}
return NS_OK;
}
void
nsDOMDeviceStorage::RemoveEventListener(const nsAString& aType,
EventListener* aListener,
bool aCapture,
ErrorResult& aRv)
{
DOMEventTargetHelper::RemoveEventListener(aType, aListener, aCapture, aRv);
if (mIsWatchingFile && !HasListenersFor(nsGkAtoms::onchange)) {
mIsWatchingFile = false;
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
obs->RemoveObserver(this, kFileWatcherUpdate);
aRv.Throw(rv);
}
}
NS_IMETHODIMP
nsDOMDeviceStorage::RemoveSystemEventListener(const nsAString & aType,
nsIDOMEventListener *aListener,
bool aUseCapture)
{
return nsDOMDeviceStorage::RemoveEventListener(aType, aListener, aUseCapture);
}
NS_IMETHODIMP
nsDOMDeviceStorage::DispatchEvent(nsIDOMEvent *aEvt,
bool *aRetval)
{
return DOMEventTargetHelper::DispatchEvent(aEvt, aRetval);
}
EventTarget*
nsDOMDeviceStorage::GetTargetForDOMEvent()
{
return DOMEventTargetHelper::GetTargetForDOMEvent();
}
EventTarget *
nsDOMDeviceStorage::GetTargetForEventTargetChain()
{
return DOMEventTargetHelper::GetTargetForEventTargetChain();
}
nsresult
nsDOMDeviceStorage::PreHandleEvent(EventChainPreVisitor& aVisitor)
{
return DOMEventTargetHelper::PreHandleEvent(aVisitor);
}
nsresult
nsDOMDeviceStorage::WillHandleEvent(EventChainPostVisitor& aVisitor)
{
return DOMEventTargetHelper::WillHandleEvent(aVisitor);
}
nsresult
nsDOMDeviceStorage::PostHandleEvent(EventChainPostVisitor& aVisitor)
{
return DOMEventTargetHelper::PostHandleEvent(aVisitor);
}
nsresult
nsDOMDeviceStorage::DispatchDOMEvent(WidgetEvent* aEvent,
nsIDOMEvent* aDOMEvent,
nsPresContext* aPresContext,
nsEventStatus* aEventStatus)
{
return DOMEventTargetHelper::DispatchDOMEvent(aEvent,
aDOMEvent,
aPresContext,
aEventStatus);
}
EventListenerManager*
nsDOMDeviceStorage::GetOrCreateListenerManager()
{
return DOMEventTargetHelper::GetOrCreateListenerManager();
}
EventListenerManager*
nsDOMDeviceStorage::GetExistingListenerManager() const
{
return DOMEventTargetHelper::GetExistingListenerManager();
}
nsIScriptContext *
nsDOMDeviceStorage::GetContextForEventHandlers(nsresult *aRv)
{
return DOMEventTargetHelper::GetContextForEventHandlers(aRv);
}
JSContext *
nsDOMDeviceStorage::GetJSContextForEventHandlers()
{
return DOMEventTargetHelper::GetJSContextForEventHandlers();
}
NS_IMPL_EVENT_HANDLER(nsDOMDeviceStorage, change)
+1 -1
View File
@@ -503,7 +503,7 @@ FMRadio::EnableAudioChannelAgent()
NS_IMETHODIMP
FMRadio::CanPlayChanged(int32_t aCanPlay)
{
SetCanPlay(aCanPlay == AudioChannelState::AUDIO_CHANNEL_STATE_NORMAL);
SetCanPlay(!(aCanPlay == AudioChannelState::AUDIO_CHANNEL_STATE_MUTED));
return NS_OK;
}
+11 -2
View File
@@ -1062,6 +1062,11 @@ static bool UseAudioChannelService()
return Preferences::GetBool("media.useAudioChannelService");
}
static bool UseAudioChannelAPI()
{
return Preferences::GetBool("media.useAudioChannelAPI");
}
void HTMLMediaElement::UpdatePreloadAction()
{
PreloadAction nextAction = PRELOAD_UNDEFINED;
@@ -4448,11 +4453,15 @@ nsresult HTMLMediaElement::UpdateChannelMuteState(AudioChannelState aCanPlay)
// We have to mute this channel.
if (aCanPlay == AUDIO_CHANNEL_STATE_MUTED && !(mMuted & MUTED_BY_AUDIO_CHANNEL)) {
SetMutedInternal(mMuted | MUTED_BY_AUDIO_CHANNEL);
DispatchAsyncEvent(NS_LITERAL_STRING("mozinterruptbegin"));
if (UseAudioChannelAPI()) {
DispatchAsyncEvent(NS_LITERAL_STRING("mozinterruptbegin"));
}
} else if (aCanPlay != AUDIO_CHANNEL_STATE_MUTED &&
(mMuted & MUTED_BY_AUDIO_CHANNEL)) {
SetMutedInternal(mMuted & ~MUTED_BY_AUDIO_CHANNEL);
DispatchAsyncEvent(NS_LITERAL_STRING("mozinterruptend"));
if (UseAudioChannelAPI()) {
DispatchAsyncEvent(NS_LITERAL_STRING("mozinterruptend"));
}
}
SuspendOrResumeElement(mMuted & MUTED_BY_AUDIO_CHANNEL, false);
-11
View File
@@ -1,11 +0,0 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
XPIDL_SOURCES += [
'nsIDOMDeviceStorage.idl',
]
XPIDL_MODULE = 'dom_devicestorage'
@@ -1,46 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "domstubs.idl"
#include "nsIDOMEventTarget.idl"
interface nsIDOMBlob;
interface nsIDOMDOMRequest;
interface nsIDOMDOMCursor;
interface nsIDOMDeviceStorageChangeEvent;
interface nsIDOMEventListener;
interface nsIFile;
%{C++
struct DeviceStorageFileDescriptor;
%}
[ptr] native DeviceStorageFdPtr(DeviceStorageFileDescriptor);
[uuid(25e4e387-1974-4f77-83b5-e6f3cf1beae8), builtinclass]
interface nsIDOMDeviceStorage : nsIDOMEventTarget
{
[implicit_jscontext] attribute jsval onchange;
nsIDOMDOMRequest add(in nsIDOMBlob aBlob);
nsIDOMDOMRequest addNamed(in nsIDOMBlob aBlob, in DOMString aName);
nsIDOMDOMRequest get([Null(Stringify)] in DOMString aName);
nsIDOMDOMRequest getEditable([Null(Stringify)] in DOMString aName);
nsIDOMDOMRequest delete([Null(Stringify)] in DOMString aName);
nsIDOMDOMRequest freeSpace();
nsIDOMDOMRequest usedSpace();
nsIDOMDOMRequest available();
// Note that the storageName is just a name (like sdcard), and doesn't
// include any path information.
readonly attribute DOMString storageName;
// Determines if this storage area is the one which will be used by default
// for storing new files.
readonly attribute bool default;
// Note: aFileDescriptor is reference counted, which is why we're using
// a pointer rather than a reference.
[noscript] nsIDOMDOMRequest createFileDescriptor(in DOMString aName,
in DeviceStorageFdPtr aFileDescriptor);
};
+10 -3
View File
@@ -293,6 +293,11 @@ static bool UseAudioChannelService()
return Preferences::GetBool("media.useAudioChannelService");
}
static bool UseAudioChannelAPI()
{
return Preferences::GetBool("media.useAudioChannelAPI");
}
class EventProxyHandler final : public nsIDOMEventListener
{
public:
@@ -543,9 +548,11 @@ AudioDestinationNode::CanPlayChanged(int32_t aCanPlay)
mAudioChannelAgentPlaying = playing;
SetCanPlay(playing);
Context()->DispatchTrustedEvent(
playing ? NS_LITERAL_STRING("mozinterruptend")
: NS_LITERAL_STRING("mozinterruptbegin"));
if (UseAudioChannelAPI()) {
Context()->DispatchTrustedEvent(
playing ? NS_LITERAL_STRING("mozinterruptend")
: NS_LITERAL_STRING("mozinterruptbegin"));
}
return NS_OK;
}
@@ -35,6 +35,7 @@ function test() {
"dom/media/webaudio/test/browser_mozAudioChannel.html";
SpecialPowers.pushPrefEnv({"set": [["media.defaultAudioChannel", "content" ],
["media.useAudioChannelAPI", true ],
["media.useAudioChannelService", true ]]},
function() {
let tab1 = gBrowser.addTab(testURL);
@@ -35,6 +35,7 @@ function test() {
"dom/media/webaudio/test/browser_mozAudioChannel_muted.html";
SpecialPowers.pushPrefEnv({"set": [["media.defaultAudioChannel", "content" ],
["media.useAudioChannelAPI", true ],
["media.useAudioChannelService", true ]]},
function() {
let tab1 = gBrowser.addTab(testURL);
@@ -141,7 +141,8 @@ function runTest() {
test();
}
SpecialPowers.pushPrefEnv({"set": [["media.useAudioChannelService", true ]]}, runTest);
SpecialPowers.pushPrefEnv({"set": [["media.useAudioChannelAPI", true ],
["media.useAudioChannelService", true ]]}, runTest);
SimpleTest.waitForExplicitFinish();
SimpleTest.requestLongerTimeout(5);
-1
View File
@@ -12,7 +12,6 @@ interfaces = [
'core',
'html',
'events',
'devicestorage',
'settings',
'stylesheets',
'sidebar',
+3 -3
View File
@@ -95,16 +95,16 @@ interface AudioContext : EventTarget {
// Mozilla extensions
partial interface AudioContext {
// Read AudioChannel.webidl for more information about this attribute.
[Pref="media.useAudioChannelService"]
[Pref="media.useAudioChannelAPI"]
readonly attribute AudioChannel mozAudioChannelType;
// These 2 events are dispatched when the AudioContext object is muted by
// the AudioChannelService. It's call 'interrupt' because when this event is
// dispatched on a HTMLMediaElement, the audio stream is paused.
[Pref="media.useAudioChannelService"]
[Pref="media.useAudioChannelAPI"]
attribute EventHandler onmozinterruptbegin;
[Pref="media.useAudioChannelService"]
[Pref="media.useAudioChannelAPI"]
attribute EventHandler onmozinterruptend;
// This method is for test only.
@@ -35,7 +35,8 @@
if (aTopic == "console-api-log-event") {
var obj = aSubject.wrappedJSObject;
is (obj.arguments[0], "Hello world from a SharedWorker!", "A message from a SharedWorker \\o/");
is (aData, "SharedWorker", "The ID is SharedWorker");
is (obj.ID, "http://mochi.test:8888/tests/dom/workers/test/sharedWorker_console.js", "The ID is SharedWorker");
is (obj.innerID, "SharedWorker", "The ID is SharedWorker");
is (order++, 1, "Then a log message.");
SpecialPowers.removeObserver(this, "console-api-log-event");
+23 -10
View File
@@ -165,6 +165,7 @@ function TabTarget(tab) {
this._handleThreadState = this._handleThreadState.bind(this);
this.on("thread-resumed", this._handleThreadState);
this.on("thread-paused", this._handleThreadState);
this.activeTab = this.activeConsole = null;
// Only real tabs need initialization here. Placeholder objects for remote
// targets will be initialized after a makeRemote method call.
if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) {
@@ -413,6 +414,19 @@ TabTarget.prototype = {
}
this.activeTab = aTabClient;
this.threadActor = aResponse.threadActor;
attachConsole();
});
};
let attachConsole = () => {
this._client.attachConsole(this._form.consoleActor,
[ "NetworkActivity" ],
(aResponse, aWebConsoleClient) => {
if (!aWebConsoleClient) {
this._remote.reject("Unable to attach to the console");
return;
}
this.activeConsole = aWebConsoleClient;
this._remote.resolve(null);
});
};
@@ -449,7 +463,7 @@ TabTarget.prototype = {
} else {
// AddonActor and chrome debugging on RootActor doesn't inherits from TabActor and
// doesn't need to be attached.
this._remote.resolve(null);
attachConsole();
}
return this._remote.promise;
@@ -603,17 +617,15 @@ TabTarget.prototype = {
// We started with a local tab and created the client ourselves, so we
// should close it.
this._client.close(cleanupAndResolve);
} else {
} else if (this.activeTab) {
// The client was handed to us, so we are not responsible for closing
// it. We just need to detach from the tab, if already attached.
if (this.activeTab) {
// |detach| may fail if the connection is already dead, so proceed
// cleanup directly after this.
this.activeTab.detach();
cleanupAndResolve();
} else {
cleanupAndResolve();
}
// |detach| may fail if the connection is already dead, so proceed with
// cleanup directly after this.
this.activeTab.detach();
cleanupAndResolve();
} else {
cleanupAndResolve();
}
}
@@ -630,6 +642,7 @@ TabTarget.prototype = {
promiseTargets.delete(this._form);
}
this.activeTab = null;
this.activeConsole = null;
this._client = null;
this._tab = null;
this._form = null;
+4 -1
View File
@@ -109,6 +109,9 @@ function Toolbox(target, selectedTool, hostType, hostOptions) {
this._toolPanels = new Map();
this._telemetry = new Telemetry();
this._initInspector = null;
this._inspector = null;
this._toolRegistered = this._toolRegistered.bind(this);
this._toolUnregistered = this._toolUnregistered.bind(this);
this._refreshHostTitle = this._refreshHostTitle.bind(this);
@@ -366,7 +369,7 @@ Toolbox.prototype = {
this._pingTelemetry();
let panel = yield this.selectTool(this._defaultToolId);
yield this.selectTool(this._defaultToolId);
// Wait until the original tool is selected so that the split
// console input will receive focus.
@@ -196,7 +196,9 @@ let NetMonitorController = {
/**
* Initiates remote or chrome network monitoring based on the current target,
* wiring event handlers as necessary.
* wiring event handlers as necessary. Since the TabTarget will have already
* started listening to network requests by now, this is largely
* netmonitor-specific initialization.
*
* @return object
* A promise that is resolved when the monitor finishes connecting.
@@ -209,15 +211,18 @@ let NetMonitorController = {
let deferred = promise.defer();
this._connection = deferred.promise;
let target = this._target;
let { client, form } = target;
this.client = this._target.client;
// Some actors like AddonActor or RootActor for chrome debugging
// do not support attach/detach and can be used directly
if (!target.isTabActor) {
this._startChromeMonitoring(client, form.consoleActor, deferred.resolve);
} else {
this._startMonitoringTab(client, form, deferred.resolve);
// aren't actual tabs.
if (this._target.isTabActor) {
this.tabClient = this._target.activeTab;
}
this.webConsoleClient = this._target.activeConsole;
this.webConsoleClient.setPreferences(NET_PREFS, () => {
this.TargetEventsHandler.connect();
this.NetworkEventsHandler.connect();
deferred.resolve();
});
yield deferred.promise;
window.emit(EVENTS.CONNECTED);
@@ -243,82 +248,6 @@ let NetMonitorController = {
return !!this.client;
},
/**
* Sets up a monitoring session.
*
* @param DebuggerClient aClient
* The debugger client.
* @param object aTabGrip
* The remote protocol grip of the tab.
* @param function aCallback
* A function to invoke once the client attached to the console client.
*/
_startMonitoringTab: function(aClient, aTabGrip, aCallback) {
if (!aClient) {
Cu.reportError("No client found!");
return;
}
this.client = aClient;
aClient.attachTab(aTabGrip.actor, (aResponse, aTabClient) => {
if (!aTabClient) {
Cu.reportError("No tab client found!");
return;
}
this.tabClient = aTabClient;
aClient.attachConsole(aTabGrip.consoleActor, LISTENERS, (aResponse, aWebConsoleClient) => {
if (!aWebConsoleClient) {
Cu.reportError("Couldn't attach to console: " + aResponse.error);
return;
}
this.webConsoleClient = aWebConsoleClient;
this.webConsoleClient.setPreferences(NET_PREFS, () => {
this.TargetEventsHandler.connect();
this.NetworkEventsHandler.connect();
if (aCallback) {
aCallback();
}
});
});
});
},
/**
* Sets up a chrome monitoring session.
*
* @param DebuggerClient aClient
* The debugger client.
* @param object aConsoleActor
* The remote protocol grip of the chrome debugger.
* @param function aCallback
* A function to invoke once the client attached to the console client.
*/
_startChromeMonitoring: function(aClient, aConsoleActor, aCallback) {
if (!aClient) {
Cu.reportError("No client found!");
return;
}
this.client = aClient;
aClient.attachConsole(aConsoleActor, LISTENERS, (aResponse, aWebConsoleClient) => {
if (!aWebConsoleClient) {
Cu.reportError("Couldn't attach to console: " + aResponse.error);
return;
}
this.webConsoleClient = aWebConsoleClient;
this.webConsoleClient.setPreferences(NET_PREFS, () => {
this.TargetEventsHandler.connect();
this.NetworkEventsHandler.connect();
if (aCallback) {
aCallback();
}
});
});
},
/**
* Gets the activity currently performed by the frontend.
* @return number
@@ -440,7 +369,6 @@ function TargetEventsHandler() {
TargetEventsHandler.prototype = {
get target() NetMonitorController._target,
get webConsoleClient() NetMonitorController.webConsoleClient,
/**
* Listen for events emitted by the current tab target.
@@ -528,8 +456,28 @@ NetworkEventsHandler.prototype = {
*/
connect: function() {
dumpn("NetworkEventsHandler is connecting...");
this.client.addListener("networkEvent", this._onNetworkEvent);
this.client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
this._displayCachedEvents();
},
/**
* Display any network events already in the cache.
*/
_displayCachedEvents: function() {
for (let cachedEvent of this.webConsoleClient.getNetworkEvents()) {
// First add the request to the timeline.
this._onNetworkEvent("networkEvent", cachedEvent);
// Then replay any updates already received.
for (let update of cachedEvent.updates) {
this._onNetworkEventUpdate("networkEventUpdate", {
packet: {
updateType: update
},
networkInfo: cachedEvent
});
}
}
},
/**
@@ -540,25 +488,21 @@ NetworkEventsHandler.prototype = {
return;
}
dumpn("NetworkEventsHandler is disconnecting...");
this.client.removeListener("networkEvent", this._onNetworkEvent);
this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
},
/**
* The "networkEvent" message type handler.
*
* @param string aType
* @param string type
* Message type.
* @param object aPacket
* The message received from the server.
* @param object networkInfo
* The network request information.
*/
_onNetworkEvent: function(aType, aPacket) {
if (aPacket.from != this.webConsoleClient.actor) {
// Skip events from different console actors.
return;
}
_onNetworkEvent: function(type, networkInfo) {
let { actor, startedDateTime, request: { method, url }, isXHR, fromCache } = networkInfo;
let { actor, startedDateTime, method, url, isXHR, fromCache } = aPacket.eventActor;
NetMonitorView.RequestsMenu.addRequest(
actor, startedDateTime, method, url, isXHR, fromCache
);
@@ -568,19 +512,17 @@ NetworkEventsHandler.prototype = {
/**
* The "networkEventUpdate" message type handler.
*
* @param string aType
* @param string type
* Message type.
* @param object aPacket
* @param object packet
* The message received from the server.
* @param object networkInfo
* The network request information.
*/
_onNetworkEventUpdate: function(aType, aPacket) {
let actor = aPacket.from;
if (!NetMonitorView.RequestsMenu.getItemByValue(actor)) {
// Skip events from unknown actors.
return;
}
_onNetworkEventUpdate: function(type, { packet, networkInfo }) {
let actor = networkInfo.actor;
switch (aPacket.updateType) {
switch (packet.updateType) {
case "requestHeaders":
this.webConsoleClient.getRequestHeaders(actor, this._onRequestHeaders);
window.emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
@@ -594,8 +536,8 @@ NetworkEventsHandler.prototype = {
window.emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
break;
case "securityInfo":
NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
securityState: aPacket.state,
NetMonitorView.RequestsMenu.updateRequest(actor, {
securityState: networkInfo.securityInfo,
});
this.webConsoleClient.getSecurityInfo(actor, this._onSecurityInfo);
window.emit(EVENTS.UPDATING_SECURITY_INFO, actor);
@@ -609,28 +551,28 @@ NetworkEventsHandler.prototype = {
window.emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
break;
case "responseStart":
NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
httpVersion: aPacket.response.httpVersion,
remoteAddress: aPacket.response.remoteAddress,
remotePort: aPacket.response.remotePort,
status: aPacket.response.status,
statusText: aPacket.response.statusText,
headersSize: aPacket.response.headersSize
NetMonitorView.RequestsMenu.updateRequest(actor, {
httpVersion: networkInfo.response.httpVersion,
remoteAddress: networkInfo.response.remoteAddress,
remotePort: networkInfo.response.remotePort,
status: networkInfo.response.status,
statusText: networkInfo.response.statusText,
headersSize: networkInfo.response.headersSize
});
window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
break;
case "responseContent":
NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
contentSize: aPacket.contentSize,
transferredSize: aPacket.transferredSize,
mimeType: aPacket.mimeType
NetMonitorView.RequestsMenu.updateRequest(actor, {
contentSize: networkInfo.response.bodySize,
transferredSize: networkInfo.response.transferredSize,
mimeType: networkInfo.response.content.mimeType
});
this.webConsoleClient.getResponseContent(actor, this._onResponseContent);
window.emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
break;
case "eventTimings":
NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
totalTime: aPacket.totalTime
NetMonitorView.RequestsMenu.updateRequest(actor, {
totalTime: networkInfo.totalTime
});
this.webConsoleClient.getEventTimings(actor, this._onEventTimings);
window.emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
@@ -15,9 +15,6 @@ function test() {
let { RequestsMenu } = NetMonitorView;
reqMenu = RequestsMenu;
is(reqMenu.itemCount, 0,
"The request menu should empty before reloading");
let button = document.querySelector("#requests-menu-reload-notice-button");
button.click();
})
+5
View File
@@ -149,6 +149,11 @@ function initNetMonitor(aUrl, aWindow, aEnableCache) {
if(!aEnableCache) {
yield toggleCache(target, true);
info("Cache disabled when the current and all future toolboxes are open.");
// Remove any requests generated by the reload while toggling the cache to
// avoid interfering with the test.
isnot([...target.activeConsole.getNetworkEvents()].length, 0,
"Request to reconfigure the tab was recorded.");
target.activeConsole.clearNetworkRequests();
}
let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
+112 -7
View File
@@ -104,6 +104,21 @@ const CATEGORY_MAPPINGS = {
* for `.marker-details-bullet.{COLORNAME}` for the equivilent
* entry in ./browser/themes/shared/devtools/performance.inc.css
* https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
* - collapseFunc: A function determining how markers are collapsed together.
* Invoked with 3 arguments: the current parent marker, the
* current marker and a method for peeking i markers ahead. If
* nothing is returned, the marker is added as a standalone entry
* in the waterfall. Otherwise, an object needs to be returned
* with the following properties:
* - toParent: The parent marker name (needs to be an entry in
* the `TIMELINE_BLUEPRINT` itself).
* - withData: An object containing some properties to staple
* on the parent marker.
* - forceNew: True if a new parent marker needs to be created
* even though there is one currently available
* with the same name.
* - forceEnd: True if the current parent marker is full after
* this collapse operation and should be finalized.
* - fields: An optional array of marker properties you wish to display in the
* marker details view. For example, a field in the array such as
* { property: "aCauseName", label: "Cause" } would render a string
@@ -127,51 +142,64 @@ const TIMELINE_BLUEPRINT = {
"Styles": {
group: 0,
colorName: "graphs-purple",
collapseFunc: collapseConsecutiveIdentical,
label: L10N.getStr("timeline.label.styles2"),
fields: getStylesFields,
},
"Reflow": {
group: 0,
colorName: "graphs-purple",
label: L10N.getStr("timeline.label.reflow2")
collapseFunc: collapseConsecutiveIdentical,
label: L10N.getStr("timeline.label.reflow2"),
},
"Paint": {
group: 0,
colorName: "graphs-green",
label: L10N.getStr("timeline.label.paint")
collapseFunc: collapseConsecutiveIdentical,
label: L10N.getStr("timeline.label.paint"),
},
/* Group 1 - JS */
"DOMEvent": {
group: 1,
colorName: "graphs-yellow",
collapseFunc: collapseDOMIntoDOMJS,
label: L10N.getStr("timeline.label.domevent"),
fields: getDOMEventFields,
},
"Javascript": {
group: 1,
colorName: "graphs-yellow",
collapseFunc: either(collapseJSIntoDOMJS, collapseConsecutiveIdentical),
label: getJSLabel,
fields: getJSFields,
},
"meta::DOMEvent+JS": {
colorName: "graphs-yellow",
label: getDOMJSLabel,
fields: getDOMEventFields,
},
"Parse HTML": {
group: 1,
colorName: "graphs-yellow",
label: L10N.getStr("timeline.label.parseHTML")
collapseFunc: collapseConsecutiveIdentical,
label: L10N.getStr("timeline.label.parseHTML"),
},
"Parse XML": {
group: 1,
colorName: "graphs-yellow",
label: L10N.getStr("timeline.label.parseXML")
collapseFunc: collapseConsecutiveIdentical,
label: L10N.getStr("timeline.label.parseXML"),
},
"GarbageCollection": {
group: 1,
colorName: "graphs-red",
collapseFunc: collapseAdjacentGC,
label: getGCLabel,
fields: [
{ property: "causeName", label: "Reason:" },
{ property: "nonincrementalReason", label: "Non-incremental Reason:" }
]
],
},
/* Group 2 - User Controlled */
@@ -182,7 +210,7 @@ const TIMELINE_BLUEPRINT = {
fields: [{
property: "causeName",
label: L10N.getStr("timeline.markerDetail.consoleTimerName")
}]
}],
},
"TimeStamp": {
group: 2,
@@ -191,10 +219,83 @@ const TIMELINE_BLUEPRINT = {
fields: [{
property: "causeName",
label: "Label:"
}]
}],
},
};
/**
* Helper for creating a function that returns the first defined result from
* a list of functions passed in as params, in order.
* @param ...function fun
* @return any
*/
function either(...fun) {
return function() {
for (let f of fun) {
let result = f.apply(null, arguments);
if (result !== undefined) return result;
}
}
}
/**
* A series of collapsers used by the blueprint. These functions are
* consecutively invoked on a moving window of two markers.
*/
function collapseConsecutiveIdentical(parent, curr, peek) {
// If there is a parent marker currently being filled and the current marker
// should go into the parent marker, make it so.
if (parent && parent.name == curr.name) {
return { toParent: parent.name };
}
// Otherwise if the current marker is the same type as the next marker type,
// create a new parent marker containing the current marker.
let next = peek(1);
if (next && curr.name == next.name) {
return { toParent: curr.name };
}
}
function collapseAdjacentGC(parent, curr, peek) {
let next = peek(1);
if (next && (next.start < curr.end || next.start - curr.end <= 10 /* ms */)) {
return collapseConsecutiveIdentical(parent, curr, peek);
}
}
function collapseDOMIntoDOMJS(parent, curr, peek) {
// If the next marker is a JavaScript marker, create a new meta parent marker
// containing the current marker.
let next = peek(1);
if (next && next.name == "Javascript") {
return {
forceNew: true,
toParent: "meta::DOMEvent+JS",
withData: {
type: curr.type,
eventPhase: curr.eventPhase
},
};
}
}
function collapseJSIntoDOMJS(parent, curr, peek) {
// If there is a parent marker currently being filled, and it's the one
// created from a `DOMEvent` via `collapseDOMIntoDOMJS`, then the current
// marker has to go into that one.
if (parent && parent.name == "meta::DOMEvent+JS") {
return {
forceEnd: true,
toParent: "meta::DOMEvent+JS",
withData: {
stack: curr.stack,
endStack: curr.endStack
},
};
}
}
/**
* A series of formatters used by the blueprint.
*/
@@ -236,6 +337,10 @@ function getJSLabel (marker={}) {
return generic;
}
function getDOMJSLabel (marker={}) {
return `Event (${marker.type})`;
}
/**
* Returns a hash for computing a fields object for a JS marker. If the cause
* is considered content (so an entry exists in the JS_MARKER_MAP), do not display it
@@ -0,0 +1,122 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* Utility functions for collapsing markers into a waterfall.
*/
loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
"devtools/performance/global", true);
/**
* Collapses markers into a tree-like structure. Currently, this only goes
* one level deep.
* @param object markerNode
* @param array markersList
*/
function collapseMarkersIntoNode({ markerNode, markersList }) {
let [getOrCreateParentNode, getCurrentParentNode, clearParentNode] = makeParentNodeFactory();
for (let i = 0, len = markersList.length; i < len; i++) {
let curr = markersList[i];
let blueprint = TIMELINE_BLUEPRINT[curr.name];
let parentNode = getCurrentParentNode();
let collapse = blueprint.collapseFunc || (() => null);
let peek = distance => markersList[i + distance];
let collapseInfo = collapse(parentNode, curr, peek);
if (collapseInfo) {
let { toParent, withData, forceNew, forceEnd } = collapseInfo;
// If the `forceNew` prop is set on the collapse info, then a new parent
// marker needs to be created even if there is one already available.
if (forceNew) {
clearParentNode();
}
// If the `toParent` prop is set on the collapse info, then this marker
// can be collapsed into a higher-level parent marker.
if (toParent) {
let parentNode = getOrCreateParentNode(markerNode, toParent, curr.start);
parentNode.end = curr.end;
parentNode.submarkers.push(curr);
for (let key in withData) {
parentNode[key] = withData[key];
}
}
// If the `forceEnd` prop is set on the collapse info, then the higher-level
// parent marker is full and should be finalized.
if (forceEnd) {
clearParentNode();
}
} else {
clearParentNode();
markerNode.submarkers.push(curr);
}
}
}
/**
* Creates an empty parent marker, which functions like a regular marker,
* but is able to hold additional child markers.
* @param string name
* @param number start [optional]
* @param number end [optional]
* @return object
*/
function makeEmptyMarkerNode(name, start, end) {
return {
name: name,
start: start,
end: end,
submarkers: []
};
}
/**
* Creates a factory for markers containing other markers.
* @return array[function]
*/
function makeParentNodeFactory() {
let marker;
return [
/**
* Gets the current parent marker for the given marker name. If it doesn't
* exist, it creates it and appends it to another parent marker.
* @param object owner
* @param string name
* @param number start
* @return object
*/
function getOrCreateParentNode(owner, name, start) {
if (marker && marker.name == name) {
return marker;
} else {
marker = makeEmptyMarkerNode(name, start);
owner.submarkers.push(marker);
return marker;
}
},
/**
* Gets the current marker marker.
* @return object
*/
function getCurrentParentNode() {
return marker;
},
/**
* Clears the current marker marker.
*/
function clearParentNode() {
marker = null;
}
];
}
exports.makeEmptyMarkerNode = makeEmptyMarkerNode;
exports.collapseMarkersIntoNode = collapseMarkersIntoNode;
@@ -8,7 +8,6 @@
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
@@ -29,27 +28,29 @@ loader.lazyRequireGetter(this, "MarkerUtils",
*/
function MarkerDetails(parent, splitter) {
EventEmitter.decorate(this);
this._onClick = this._onClick.bind(this);
this._document = parent.ownerDocument;
this._parent = parent;
this._splitter = splitter;
this._splitter.addEventListener("mouseup", () => this.emit("resize"));
this._onClick = this._onClick.bind(this);
this._onSplitterMouseUp = this._onSplitterMouseUp.bind(this);
this._parent.addEventListener("click", this._onClick);
this._splitter.addEventListener("mouseup", this._onSplitterMouseUp);
}
MarkerDetails.prototype = {
/**
* Removes any node references from this view.
* Sets this view's width.
* @param boolean
*/
destroy: function() {
this.empty();
this._parent.removeEventListener("click", this._onClick);
this._parent = null;
this._splitter = null;
set width(value) {
this._parent.setAttribute("width", value);
},
/**
* Clears the view.
* Clears the marker details from this view.
*/
empty: function() {
this._parent.innerHTML = "";
@@ -60,8 +61,8 @@ MarkerDetails.prototype = {
*
* @param object params
* An options object holding:
* marker - The marker to display.
* frames - Array of stack frame information; see stack.js.
* - marker: The marker to display.
* - frames: Array of stack frame information; see stack.js.
*/
render: function({ marker, frames }) {
this.empty();
@@ -69,10 +70,10 @@ MarkerDetails.prototype = {
let elements = [];
elements.push(MarkerUtils.DOM.buildTitle(this._document, marker));
elements.push(MarkerUtils.DOM.buildDuration(this._document, marker));
MarkerUtils.DOM.buildFields(this._document, marker).forEach(field => elements.push(field));
MarkerUtils.DOM.buildFields(this._document, marker).forEach(f => elements.push(f));
// Build a stack element -- and use the "startStack" label if
// we have both a star and endStack.
// we have both a startStack and endStack.
if (marker.stack) {
let type = marker.endStack ? "startStack" : "stack";
elements.push(MarkerUtils.DOM.buildStackTrace(this._document, {
@@ -98,6 +99,13 @@ MarkerDetails.prototype = {
this.emit("view-source", data.url, data.line);
}
},
/**
* Handles the "mouseup" event on the marker details view splitter.
*/
_onSplitterMouseUp: function() {
this.emit("resize");
}
};
/**
@@ -0,0 +1,305 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* This file contains the "marker" view, essentially a detailed list
* of all the markers in the timeline data.
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
const { Heritage } = require("resource:///modules/devtools/ViewHelpers.jsm");
const { AbstractTreeItem } = require("resource:///modules/devtools/AbstractTreeItem.jsm");
const { TIMELINE_BLUEPRINT: ORIGINAL_BP } = require("devtools/performance/global");
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const LEVEL_INDENT = 10; // px
const ARROW_NODE_OFFSET = -15; // px
const WATERFALL_MARKER_SIDEBAR_WIDTH = 175; // px
const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5; // px
/**
* A detailed waterfall view for the timeline data.
*
* @param MarkerView owner
* The MarkerView considered the "owner" marker. This newly created
* instance will be represent the "submarker". Should be null for root nodes.
* @param object marker
* Details about this marker, like { name, start, end, submarkers } etc.
* @param number level [optional]
* The indentation level in the waterfall tree. The root node is at level 0.
* @param boolean hidden [optional]
* Whether this node should be hidden and not contribute to depth/level
* calculations. Defaults to false.
*/
function MarkerView({ owner, marker, level, hidden }) {
AbstractTreeItem.call(this, {
parent: owner,
level: level|0 - (hidden ? 1 : 0)
});
this.marker = marker;
this.hidden = !!hidden;
this._onItemBlur = this._onItemBlur.bind(this);
this._onItemFocus = this._onItemFocus.bind(this);
}
MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
/**
* Calculates and stores the available width for the waterfall.
* This should be invoked every time the container node is resized.
*/
recalculateBounds: function() {
this.root._waterfallWidth = this.bounds.width - WATERFALL_MARKER_SIDEBAR_WIDTH;
},
/**
* Sets a list of names and colors used to paint markers.
* @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
* @param object blueprint
*/
set blueprint(blueprint) {
this.root._blueprint = blueprint;
},
get blueprint() {
return this.root._blueprint;
},
/**
* Sets the { startTime, endTime }, in milliseconds.
* @param object interval
*/
set interval(interval) {
this.root._interval = interval;
},
get interval() {
return this.root._interval;
},
/**
* Gets the current waterfall width.
* @return number
*/
getWaterfallWidth: function() {
return this._waterfallWidth;
},
/**
* Gets the data scale amount for the current width and interval.
* @return number
*/
getDataScale: function() {
let startTime = this.root._interval.startTime|0;
let endTime = this.root._interval.endTime|0;
return this.root._waterfallWidth / (endTime - startTime);
},
/**
* Creates the view for this waterfall node.
* @param nsIDOMNode document
* @param nsIDOMNode arrowNode
* @return nsIDOMNode
*/
_displaySelf: function(document, arrowNode) {
let targetNode = document.createElement("hbox");
targetNode.className = "waterfall-tree-item";
if (this == this.root) {
// Bounds are needed for properly positioning and scaling markers in
// the waterfall, but it's sufficient to make those calculations only
// for the root node.
this.root.recalculateBounds();
// The AbstractTreeItem propagates events to the root, so we don't
// need to listen them on descendant items in the tree.
this._addEventListeners();
} else {
// Root markers are an implementation detail and shouldn't be shown.
this._buildMarkerCells(document, targetNode, arrowNode);
}
if (this.hidden) {
targetNode.style.display = "none";
}
return targetNode;
},
/**
* Populates this node in the waterfall tree with the corresponding "markers".
* @param array:AbstractTreeItem children
*/
_populateSelf: function(children) {
let submarkers = this.marker.submarkers;
if (!submarkers || !submarkers.length) {
return;
}
let blueprint = this.root._blueprint;
let startTime = this.root._interval.startTime;
let endTime = this.root._interval.endTime;
let newLevel = this.level + 1;
for (let i = 0, len = submarkers.length; i < len; i++) {
let marker = submarkers[i];
// If this marker isn't in the global timeline blueprint, don't display
// it, but dump a warning message to the console.
if (!(marker.name in blueprint)) {
if (!(marker.name in ORIGINAL_BP)) {
console.warn(`Marker not found in timeline blueprint: ${marker.name}.`);
}
continue;
}
if (!isMarkerInRange(marker, startTime|0, endTime|0)) {
continue;
}
children.push(new MarkerView({
owner: this,
marker: marker,
level: newLevel,
inverted: this.inverted
}));
}
},
/**
* Builds all the nodes representing a marker in the waterfall.
* @param nsIDOMNode document
* @param nsIDOMNode targetNode
* @param nsIDOMNode arrowNode
*/
_buildMarkerCells: function(doc, targetNode, arrowNode) {
// Root markers are an implementation detail and shouldn't be shown.
let marker = this.marker;
if (marker.name == "(root)") {
return;
}
let style = this.root._blueprint[marker.name];
let startTime = this.root._interval.startTime;
let endTime = this.root._interval.endTime;
let sidebarCell = this._buildMarkerSidebar(
doc, style, marker);
let timebarCell = this._buildMarkerTimebar(
doc, style, marker, startTime, endTime, arrowNode);
targetNode.appendChild(sidebarCell);
targetNode.appendChild(timebarCell);
// Don't render an expando-arrow for leaf nodes.
let submarkers = this.marker.submarkers;
let hasDescendants = submarkers && submarkers.length > 0;
if (hasDescendants) {
targetNode.setAttribute("expandable", "");
} else {
arrowNode.setAttribute("invisible", "");
}
targetNode.setAttribute("level", this.level);
},
/**
* Functions creating each cell in this waterfall view.
* Invoked by `_displaySelf`.
*/
_buildMarkerSidebar: function(doc, style, marker) {
let cell = doc.createElement("hbox");
cell.className = "waterfall-sidebar theme-sidebar";
cell.setAttribute("width", WATERFALL_MARKER_SIDEBAR_WIDTH);
cell.setAttribute("align", "center");
let bullet = doc.createElement("hbox");
bullet.className = `waterfall-marker-bullet marker-color-${style.colorName}`;
bullet.style.transform = `translateX(${this.level * LEVEL_INDENT}px)`;
bullet.setAttribute("type", marker.name);
cell.appendChild(bullet);
let name = doc.createElement("description");
let label = MarkerUtils.getMarkerLabel(marker);
name.className = "plain waterfall-marker-name";
name.style.transform = `translateX(${this.level * LEVEL_INDENT}px)`;
name.setAttribute("crop", "end");
name.setAttribute("flex", "1");
name.setAttribute("value", label);
name.setAttribute("tooltiptext", label);
cell.appendChild(name);
return cell;
},
_buildMarkerTimebar: function(doc, style, marker, startTime, endTime, arrowNode) {
let cell = doc.createElement("hbox");
cell.className = "waterfall-marker waterfall-background-ticks";
cell.setAttribute("align", "center");
cell.setAttribute("flex", "1");
let dataScale = this.getDataScale();
let offset = (marker.start - startTime) * dataScale;
let width = (marker.end - marker.start) * dataScale;
arrowNode.style.transform =`translateX(${offset + ARROW_NODE_OFFSET}px)`;
cell.appendChild(arrowNode);
let bar = doc.createElement("hbox");
bar.className = `waterfall-marker-bar marker-color-${style.colorName}`;
bar.style.transform = `translateX(${offset}px)`;
bar.setAttribute("type", marker.name);
bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_TIMEBAR_WIDTH_MIN));
cell.appendChild(bar);
return cell;
},
/**
* Adds the event listeners for this particular tree item.
*/
_addEventListeners: function() {
this.on("focus", this._onItemFocus);
this.on("blur", this._onItemBlur);
},
/**
* Handler for the "blur" event on the root item.
*/
_onItemBlur: function() {
this.root.emit("unselected");
},
/**
* Handler for the "mousedown" event on the root item.
*/
_onItemFocus: function(e, item) {
this.root.emit("selected", item.marker);
}
});
/**
* Checks if a given marker is in the specified time range.
*
* @param object e
* The marker containing the { start, end } timestamps.
* @param number start
* The earliest allowed time.
* @param number end
* The latest allowed time.
* @return boolean
* True if the marker fits inside the specified time range.
*/
function isMarkerInRange(e, start, end) {
let m_start = e.start|0;
let m_end = e.end|0;
return (m_start >= start && m_end <= end) || // bounds inside
(m_start < start && m_end > end) || // bounds outside
(m_start < start && m_end >= start && m_end <= end) || // overlap start
(m_end > end && m_start >= start && m_start <= end); // overlap end
}
exports.MarkerView = MarkerView;
exports.WATERFALL_MARKER_SIDEBAR_WIDTH = WATERFALL_MARKER_SIDEBAR_WIDTH;
@@ -19,6 +19,8 @@ loader.lazyRequireGetter(this, "getColor",
"devtools/shared/theme", true);
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "TickUtils",
"devtools/performance/waterfall-ticks", true);
const OVERVIEW_HEADER_HEIGHT = 14; // px
const OVERVIEW_ROW_HEIGHT = 11; // px
@@ -75,7 +77,7 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
for (let type in blueprint) {
this._paintBatches.set(type, { style: blueprint[type], batch: [] });
this._lastGroup = Math.max(this._lastGroup, blueprint[type].group);
this._lastGroup = Math.max(this._lastGroup, blueprint[type].group || 0);
}
},
@@ -143,7 +145,12 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
let tickInterval = this._findOptimalTickInterval(dataScale);
let tickInterval = TickUtils.findOptimalTickInterval({
ticksMultiple: OVERVIEW_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: OVERVIEW_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
ctx.textBaseline = "middle";
ctx.font = fontSize + "px " + fontFamily;
@@ -190,32 +197,6 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
return canvas;
},
/**
* Finds the optimal tick interval between time markers in this overview.
*/
_findOptimalTickInterval: function(dataScale) {
let timingStep = OVERVIEW_HEADER_TICKS_MULTIPLE;
let spacingMin = OVERVIEW_HEADER_TICKS_SPACING_MIN * this._pixelRatio;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > spacingMin) {
return dataScale;
}
while (true) {
let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < spacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
},
/**
* Sets the theme via `theme` to either "light" or "dark",
* and updates the internal styling to match. Requires a redraw
@@ -56,11 +56,11 @@ const sum = vals => vals.reduce((a, b) => a + b, 0);
* parent node is used for all rows.
*
* @param CallView caller
* The CallView considered the "caller" frame. This instance will be
* represent the "callee". Should be null for root nodes.
* The CallView considered the "caller" frame. This newly created
* instance will be represent the "callee". Should be null for root nodes.
* @param ThreadNode | FrameNode frame
* Details about this function, like { samples, duration, calls } etc.
* @param number level
* @param number level [optional]
* The indentation level in the call tree. The root node is at level 0.
* @param boolean hidden [optional]
* Whether this node should be hidden and not contribute to depth/level
@@ -121,34 +121,32 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
* @return nsIDOMNode
*/
_displaySelf: function(document, arrowNode) {
this.document = document;
let displayedData = this.getDisplayedData();
let frameInfo = this.frame.getInfo();
if (this.visibleCells.duration) {
var durationCell = this._createTimeCell(displayedData.totalDuration);
var durationCell = this._createTimeCell(document, displayedData.totalDuration);
}
if (this.visibleCells.selfDuration) {
var selfDurationCell = this._createTimeCell(displayedData.selfDuration, true);
var selfDurationCell = this._createTimeCell(document, displayedData.selfDuration, true);
}
if (this.visibleCells.percentage) {
var percentageCell = this._createExecutionCell(displayedData.totalPercentage);
var percentageCell = this._createExecutionCell(document, displayedData.totalPercentage);
}
if (this.visibleCells.selfPercentage) {
var selfPercentageCell = this._createExecutionCell(displayedData.selfPercentage, true);
var selfPercentageCell = this._createExecutionCell(document, displayedData.selfPercentage, true);
}
if (this.visibleCells.allocations) {
var allocationsCell = this._createAllocationsCell(displayedData.totalAllocations);
var allocationsCell = this._createAllocationsCell(document, displayedData.totalAllocations);
}
if (this.visibleCells.selfAllocations) {
var selfAllocationsCell = this._createAllocationsCell(displayedData.selfAllocations, true);
var selfAllocationsCell = this._createAllocationsCell(document, displayedData.selfAllocations, true);
}
if (this.visibleCells.samples) {
var samplesCell = this._createSamplesCell(displayedData.samples);
var samplesCell = this._createSamplesCell(document, displayedData.samples);
}
if (this.visibleCells.function) {
var functionCell = this._createFunctionCell(arrowNode, displayedData.name, frameInfo, this.level);
var functionCell = this._createFunctionCell(document, arrowNode, displayedData.name, frameInfo, this.level);
}
let targetNode = document.createElement("hbox");
@@ -214,55 +212,59 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
* Functions creating each cell in this call view.
* Invoked by `_displaySelf`.
*/
_createTimeCell: function(duration, isSelf = false) {
let cell = this.document.createElement("label");
_createTimeCell: function(doc, duration, isSelf = false) {
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", isSelf ? "self-duration" : "duration");
cell.setAttribute("crop", "end");
cell.setAttribute("value", L10N.numberWithDecimals(duration, 2) + " " + MILLISECOND_UNITS);
return cell;
},
_createExecutionCell: function(percentage, isSelf = false) {
let cell = this.document.createElement("label");
_createExecutionCell: function(doc, percentage, isSelf = false) {
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", isSelf ? "self-percentage" : "percentage");
cell.setAttribute("crop", "end");
cell.setAttribute("value", L10N.numberWithDecimals(percentage, 2) + PERCENTAGE_UNITS);
return cell;
},
_createAllocationsCell: function(count, isSelf = false) {
let cell = this.document.createElement("label");
_createAllocationsCell: function(doc, count, isSelf = false) {
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", isSelf ? "self-allocations" : "allocations");
cell.setAttribute("crop", "end");
cell.setAttribute("value", count || 0);
return cell;
},
_createSamplesCell: function(count) {
let cell = this.document.createElement("label");
_createSamplesCell: function(doc, count) {
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", "samples");
cell.setAttribute("crop", "end");
cell.setAttribute("value", count || "");
return cell;
},
_createFunctionCell: function(arrowNode, frameName, frameInfo, frameLevel) {
let cell = this.document.createElement("hbox");
_createFunctionCell: function(doc, arrowNode, frameName, frameInfo, frameLevel) {
let cell = doc.createElement("hbox");
cell.className = "call-tree-cell";
cell.style.MozMarginStart = (frameLevel * CALL_TREE_INDENTATION) + "px";
cell.setAttribute("type", "function");
cell.appendChild(arrowNode);
let nameNode = this.document.createElement("label");
nameNode.className = "plain call-tree-name";
nameNode.setAttribute("flex", "1");
nameNode.setAttribute("crop", "end");
nameNode.setAttribute("value", frameName);
cell.appendChild(nameNode);
// Don't render a name label node if there's no function name. A different
// location label node will be rendered instead.
if (frameName) {
let nameNode = doc.createElement("description");
nameNode.className = "plain call-tree-name";
nameNode.setAttribute("flex", "1");
nameNode.setAttribute("crop", "end");
nameNode.setAttribute("value", frameName);
cell.appendChild(nameNode);
}
// Don't render detailed labels for meta category frames
if (!frameInfo.isMetaCategory) {
this._appendFunctionDetailsCells(cell, frameInfo);
this._appendFunctionDetailsCells(doc, cell, frameInfo);
}
// Don't render an expando-arrow for leaf nodes.
@@ -273,40 +275,50 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
return cell;
},
_appendFunctionDetailsCells: function(cell, frameInfo) {
let urlNode = this.document.createElement("label");
urlNode.className = "plain call-tree-url";
urlNode.setAttribute("flex", "1");
urlNode.setAttribute("crop", "end");
urlNode.setAttribute("value", frameInfo.fileName || "");
urlNode.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + frameInfo.url);
urlNode.addEventListener("mousedown", this._onUrlClick);
cell.appendChild(urlNode);
_appendFunctionDetailsCells: function(doc, cell, frameInfo) {
if (frameInfo.fileName) {
let urlNode = doc.createElement("description");
urlNode.className = "plain call-tree-url";
urlNode.setAttribute("flex", "1");
urlNode.setAttribute("crop", "end");
urlNode.setAttribute("value", frameInfo.fileName);
urlNode.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + frameInfo.url);
urlNode.addEventListener("mousedown", this._onUrlClick);
cell.appendChild(urlNode);
}
let lineNode = this.document.createElement("label");
lineNode.className = "plain call-tree-line";
lineNode.setAttribute("value", frameInfo.line ? ":" + frameInfo.line : "");
cell.appendChild(lineNode);
if (frameInfo.line) {
let lineNode = doc.createElement("description");
lineNode.className = "plain call-tree-line";
lineNode.setAttribute("value", ":" + frameInfo.line);
cell.appendChild(lineNode);
}
let columnNode = this.document.createElement("label");
columnNode.className = "plain call-tree-column";
columnNode.setAttribute("value", frameInfo.column ? ":" + frameInfo.column : "");
cell.appendChild(columnNode);
if (frameInfo.column) {
let columnNode = doc.createElement("description");
columnNode.className = "plain call-tree-column";
columnNode.setAttribute("value", ":" + frameInfo.column);
cell.appendChild(columnNode);
}
let hostNode = this.document.createElement("label");
hostNode.className = "plain call-tree-host";
hostNode.setAttribute("value", frameInfo.host || "");
cell.appendChild(hostNode);
if (frameInfo.host) {
let hostNode = doc.createElement("description");
hostNode.className = "plain call-tree-host";
hostNode.setAttribute("value", frameInfo.host);
cell.appendChild(hostNode);
}
let spacerNode = this.document.createElement("spacer");
let spacerNode = doc.createElement("spacer");
spacerNode.setAttribute("flex", "10000");
cell.appendChild(spacerNode);
let categoryNode = this.document.createElement("label");
categoryNode.className = "plain call-tree-category";
categoryNode.style.color = frameInfo.categoryData.color;
categoryNode.setAttribute("value", frameInfo.categoryData.label || "");
cell.appendChild(categoryNode);
if (frameInfo.categoryData.label) {
let categoryNode = doc.createElement("description");
categoryNode.className = "plain call-tree-category";
categoryNode.style.color = frameInfo.categoryData.color;
categoryNode.setAttribute("value", frameInfo.categoryData.label);
cell.appendChild(categoryNode);
}
},
/**
@@ -0,0 +1,187 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* This file contains the "waterfall ticks" view, a header for the
* markers displayed in the waterfall.
*/
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "WATERFALL_MARKER_SIDEBAR_WIDTH",
"devtools/performance/marker-view", true);
const HTML_NS = "http://www.w3.org/1999/xhtml";
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
const WATERFALL_HEADER_TEXT_PADDING = 3; // px
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
/**
* A header for a markers waterfall.
*
* @param MarkerView root
* The root item of the waterfall tree.
*/
function WaterfallHeader(root) {
this.root = root;
}
WaterfallHeader.prototype = {
/**
* Creates and appends this header as the first element of the specified
* parent element.
*
* @param nsIDOMNode parentNode
* The parent element for this header.
*/
attachTo: function(parentNode) {
let document = parentNode.ownerDocument;
let startTime = this.root.interval.startTime;
let dataScale = this.root.getDataScale();
let waterfallWidth = this.root.getWaterfallWidth();
let header = this._buildNode(document, startTime, dataScale, waterfallWidth);
parentNode.insertBefore(header, parentNode.firstChild);
this._drawWaterfallBackground(document, dataScale, waterfallWidth);
},
/**
* Creates the node displaying this view.
*/
_buildNode: function(doc, startTime, dataScale, waterfallWidth) {
let container = doc.createElement("hbox");
container.className = "waterfall-header-container";
container.setAttribute("flex", "1");
let sidebar = doc.createElement("hbox");
sidebar.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_MARKER_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
container.appendChild(sidebar);
let name = doc.createElement("description");
name.className = "plain waterfall-header-name";
name.setAttribute("value", L10N.getStr("timeline.records"));
sidebar.appendChild(name);
let ticks = doc.createElement("hbox");
ticks.className = "waterfall-header-ticks waterfall-background-ticks";
ticks.setAttribute("align", "center");
ticks.setAttribute("flex", "1");
container.appendChild(ticks);
let tickInterval = findOptimalTickInterval({
ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
for (let x = 0; x < waterfallWidth; x += tickInterval) {
let left = x + WATERFALL_HEADER_TEXT_PADDING;
let time = Math.round(x / dataScale + startTime);
let label = L10N.getFormatStr("timeline.tick", time);
let node = doc.createElement("description");
node.className = "plain waterfall-header-tick";
node.style.transform = "translateX(" + left + "px)";
node.setAttribute("value", label);
ticks.appendChild(node);
}
return container;
},
/**
* Creates the background displayed on the marker's waterfall.
*/
_drawWaterfallBackground: function(doc, dataScale, waterfallWidth) {
if (!this._canvas || !this._ctx) {
this._canvas = doc.createElementNS(HTML_NS, "canvas");
this._ctx = this._canvas.getContext("2d");
}
let canvas = this._canvas;
let ctx = this._ctx;
// Nuke the context.
let canvasWidth = canvas.width = waterfallWidth;
let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
// Start over.
let imageData = ctx.createImageData(canvasWidth, canvasHeight);
let pixelArray = imageData.data;
let buf = new ArrayBuffer(pixelArray.length);
let view8bit = new Uint8ClampedArray(buf);
let view32bit = new Uint32Array(buf);
// Build new millisecond tick lines...
let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
let tickInterval = findOptimalTickInterval({
ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
dataScale: dataScale
});
// Insert one pixel for each division on each scale.
for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
let increment = tickInterval * Math.pow(2, i);
for (let x = 0; x < canvasWidth; x += increment) {
let position = x | 0;
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
}
alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
}
// Flush the image data and cache the waterfall background.
pixelArray.set(view8bit);
ctx.putImageData(imageData, 0, 0);
doc.mozSetImageElement("waterfall-background", canvas);
}
};
/**
* Finds the optimal tick interval between time markers in this timeline.
*
* @param number ticksMultiple
* @param number ticksSpacingMin
* @param number dataScale
* @return number
*/
function findOptimalTickInterval({ ticksMultiple, ticksSpacingMin, dataScale }) {
let timingStep = ticksMultiple;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > ticksSpacingMin) {
return dataScale;
}
while (true) {
let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < ticksSpacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
}
exports.WaterfallHeader = WaterfallHeader;
exports.TickUtils = { findOptimalTickInterval };
@@ -1,620 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* This file contains the "waterfall" view, essentially a detailed list
* of all the markers in the timeline data.
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");
loader.lazyImporter(this, "setNamedTimeout",
"resource:///modules/devtools/ViewHelpers.jsm");
loader.lazyImporter(this, "clearNamedTimeout",
"resource:///modules/devtools/ViewHelpers.jsm");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const WATERFALL_SIDEBAR_WIDTH = 200; // px
const WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
const WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
const WATERFALL_HEADER_TEXT_PADDING = 3; // px
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
const WATERFALL_MARKER_BAR_WIDTH_MIN = 5; // px
const WATERFALL_ROWCOUNT_ONPAGEUPDOWN = 10;
/**
* A detailed waterfall view for the timeline data.
*
* @param nsIDOMNode parent
* The parent node holding the waterfall.
* @param nsIDOMNode container
* The container node that key events should be bound to.
* @param Object blueprint
* List of names and colors defining markers.
*/
function Waterfall(parent, container, blueprint) {
EventEmitter.decorate(this);
this._parent = parent;
this._document = parent.ownerDocument;
this._container = container;
this._fragment = this._document.createDocumentFragment();
this._outstandingMarkers = [];
this._headerContents = this._document.createElement("hbox");
this._headerContents.className = "waterfall-header-contents";
this._parent.appendChild(this._headerContents);
this._listContents = this._document.createElement("vbox");
this._listContents.className = "waterfall-list-contents";
this._listContents.setAttribute("flex", "1");
this._parent.appendChild(this._listContents);
this.setupKeys();
this._isRTL = this._getRTL();
// Lazy require is a bit slow, and these are hot objects.
this._l10n = L10N;
this._blueprint = blueprint;
this._setNamedTimeout = setNamedTimeout;
this._clearNamedTimeout = clearNamedTimeout;
// Selected row index. By default, we want the first
// row to be selected.
this._selectedRowIdx = 0;
// Default rowCount
this.rowCount = WATERFALL_ROWCOUNT_ONPAGEUPDOWN;
}
Waterfall.prototype = {
/**
* Removes any node references from this view.
*/
destroy: function() {
this._parent = this._document = this._container = null;
},
/**
* Populates this view with the provided data source.
*
* @param object data
* An object containing the following properties:
* - markers: a list of markers received from the controller
* - interval: the { startTime, endTime }, in milliseconds
*/
setData: function({ markers, interval }) {
this.clearView();
this._markers = markers;
this._interval = interval;
let { startTime, endTime } = interval;
let dataScale = this._waterfallWidth / (endTime - startTime);
this._drawWaterfallBackground(dataScale);
this._buildHeader(this._headerContents, startTime, dataScale);
this._buildMarkers(this._listContents, markers, startTime, endTime, dataScale);
this.selectRow(this._selectedRowIdx);
},
/**
* List of names and colors used to paint markers.
* @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
*/
setBlueprint: function(blueprint) {
this._blueprint = blueprint;
},
/**
* Keybindings.
*/
setupKeys: function() {
let pane = this._container;
pane.addEventListener("keydown", e => {
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx - 1);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx + 1);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_HOME) {
e.preventDefault();
this.selectNearestRow(0);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_END) {
e.preventDefault();
this.selectNearestRow(this._listContents.children.length);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx - this.rowCount);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx + this.rowCount);
}
}, true);
},
/**
* Depopulates this view.
*/
clearView: function() {
while (this._headerContents.hasChildNodes()) {
this._headerContents.firstChild.remove();
}
while (this._listContents.hasChildNodes()) {
this._listContents.firstChild.remove();
}
this._listContents.scrollTop = 0;
this._outstandingMarkers.length = 0;
this._clearNamedTimeout("flush-outstanding-markers");
},
/**
* Calculates and stores the available width for the waterfall.
* This should be invoked every time the container window is resized.
*/
recalculateBounds: function() {
let bounds = this._parent.getBoundingClientRect();
this._waterfallWidth = bounds.width - WATERFALL_SIDEBAR_WIDTH;
},
/**
* Creates the header part of this view.
*
* @param nsIDOMNode parent
* The parent node holding the header.
* @param number startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* The time scale of the data source.
*/
_buildHeader: function(parent, startTime, dataScale) {
let container = this._document.createElement("hbox");
container.className = "waterfall-header-container";
container.setAttribute("flex", "1");
let sidebar = this._document.createElement("hbox");
sidebar.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
container.appendChild(sidebar);
let name = this._document.createElement("label");
name.className = "plain waterfall-header-name";
name.setAttribute("value", this._l10n.getStr("timeline.records"));
sidebar.appendChild(name);
let ticks = this._document.createElement("hbox");
ticks.className = "waterfall-header-ticks waterfall-background-ticks";
ticks.setAttribute("align", "center");
ticks.setAttribute("flex", "1");
container.appendChild(ticks);
let offset = this._isRTL ? this._waterfallWidth : 0;
let direction = this._isRTL ? -1 : 1;
let tickInterval = this._findOptimalTickInterval({
ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
for (let x = 0; x < this._waterfallWidth; x += tickInterval) {
let left = x + direction * WATERFALL_HEADER_TEXT_PADDING;
let time = Math.round(x / dataScale + startTime);
let label = this._l10n.getFormatStr("timeline.tick", time);
let node = this._document.createElement("label");
node.className = "plain waterfall-header-tick";
node.style.transform = "translateX(" + (left - offset) + "px)";
node.setAttribute("value", label);
ticks.appendChild(node);
}
parent.appendChild(container);
},
/**
* Creates the markers part of this view.
*
* @param nsIDOMNode parent
* The parent node holding the markers.
* @param number startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* The time scale of the data source.
*/
_buildMarkers: function(parent, markers, startTime, endTime, dataScale) {
let rowsCount = 0;
let markerIdx = -1;
for (let marker of markers) {
markerIdx++;
if (!isMarkerInRange(marker, startTime, endTime)) {
continue;
}
if (!(marker.name in this._blueprint)) {
continue;
}
// Only build and display a finite number of markers initially, to
// preserve a snappy UI. After a certain delay, continue building the
// outstanding markers while there's (hopefully) no user interaction.
let arguments_ = [this._fragment, marker, startTime, dataScale, markerIdx, rowsCount];
if (rowsCount++ < WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT) {
this._buildMarker.apply(this, arguments_);
} else {
this._outstandingMarkers.push(arguments_);
}
}
// If there are no outstanding markers, add a dummy "spacer" at the end
// to fill up any remaining available space in the UI.
if (!this._outstandingMarkers.length) {
this._buildMarker(this._fragment, null);
}
// Otherwise prepare flushing the outstanding markers after a small delay.
else {
let delay = WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY;
let func = () => this._buildOutstandingMarkers(parent);
this._setNamedTimeout("flush-outstanding-markers", delay, func);
}
parent.appendChild(this._fragment);
},
/**
* Finishes building the outstanding markers in this view.
* @see Waterfall.prototype._buildMarkers
*/
_buildOutstandingMarkers: function(parent) {
if (!this._outstandingMarkers.length) {
return;
}
for (let args of this._outstandingMarkers) {
this._buildMarker.apply(this, args);
}
this._outstandingMarkers.length = 0;
parent.appendChild(this._fragment);
this.selectRow(this._selectedRowIdx);
},
/**
* Creates a single marker in this view.
*
* @param nsIDOMNode parent
* The parent node holding the marker.
* @param object marker
* The { name, start, end } marker in the data source.
* @param startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
* @param number markerIdx
* Index of the marker in this._markers
* @param number rowIdx
* Index of current row
*/
_buildMarker: function(parent, marker, startTime, dataScale, markerIdx, rowIdx) {
let container = this._document.createElement("hbox");
container.setAttribute("markerIdx", markerIdx);
container.className = "waterfall-marker-container";
if (marker) {
this._buildMarkerSidebar(container, marker);
this._buildMarkerWaterfall(container, marker, startTime, dataScale, markerIdx);
container.onclick = () => this.selectRow(rowIdx);
} else {
this._buildMarkerSpacer(container);
container.setAttribute("flex", "1");
container.setAttribute("is-spacer", "");
}
parent.appendChild(container);
},
/**
* Select first row.
*/
resetSelection: function() {
this.selectRow(0);
},
/**
* Select a marker in the waterfall.
*
* @param number idx
* Index of the row to select. -1 clears the selection.
*/
selectRow: function(idx) {
let prev = this._listContents.children[this._selectedRowIdx];
if (prev) {
prev.classList.remove("selected");
}
this._selectedRowIdx = idx;
let row = this._listContents.children[idx];
if (row && !row.hasAttribute("is-spacer")) {
row.focus();
row.classList.add("selected");
let markerIdx = row.getAttribute("markerIdx");
this.emit("selected", this._markers[markerIdx]);
this.ensureRowIsVisible(row);
} else {
this.emit("unselected");
}
},
/**
* Find a valid row to select.
*
* @param number idx
* Index of the row to select.
*/
selectNearestRow: function(idx) {
if (this._listContents.children.length == 0) {
return;
}
idx = Math.max(idx, 0);
idx = Math.min(idx, this._listContents.children.length - 1);
let row = this._listContents.children[idx];
if (row && row.hasAttribute("is-spacer")) {
if (idx > 0) {
return this.selectNearestRow(idx - 1);
} else {
return;
}
}
this.selectRow(idx);
},
/**
* Scroll waterfall to ensure row is in the viewport.
*
* @param number idx
* Index of the row to select.
*/
ensureRowIsVisible: function(row) {
let parent = row.parentNode;
let parentRect = parent.getBoundingClientRect();
let rowRect = row.getBoundingClientRect();
let yDelta = rowRect.top - parentRect.top;
if (yDelta < 0) {
parent.scrollTop += yDelta;
}
yDelta = parentRect.bottom - rowRect.bottom;
if (yDelta < 0) {
parent.scrollTop -= yDelta;
}
},
/**
* Creates the sidebar part of a marker in this view.
*
* @param nsIDOMNode container
* The container node representing the marker in this view.
* @param object marker
* @see Waterfall.prototype._buildMarker
*/
_buildMarkerSidebar: function(container, marker) {
let blueprint = this._blueprint[marker.name];
let sidebar = this._document.createElement("hbox");
sidebar.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
let bullet = this._document.createElement("hbox");
bullet.className = `waterfall-marker-bullet marker-color-${blueprint.colorName}`;
bullet.setAttribute("type", marker.name);
sidebar.appendChild(bullet);
let name = this._document.createElement("label");
name.setAttribute("crop", "end");
name.setAttribute("flex", "1");
name.className = "plain waterfall-marker-name";
let label = MarkerUtils.getMarkerLabel(marker);
name.setAttribute("value", label);
name.setAttribute("tooltiptext", label);
sidebar.appendChild(name);
container.appendChild(sidebar);
},
/**
* Creates the waterfall part of a marker in this view.
*
* @param nsIDOMNode container
* The container node representing the marker.
* @param object marker
* @see Waterfall.prototype._buildMarker
* @param startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
*/
_buildMarkerWaterfall: function(container, marker, startTime, dataScale) {
let blueprint = this._blueprint[marker.name];
let waterfall = this._document.createElement("hbox");
waterfall.className = "waterfall-marker-item waterfall-background-ticks";
waterfall.setAttribute("align", "center");
waterfall.setAttribute("flex", "1");
let start = (marker.start - startTime) * dataScale;
let width = (marker.end - marker.start) * dataScale;
let offset = this._isRTL ? this._waterfallWidth : 0;
let bar = this._document.createElement("hbox");
bar.className = `waterfall-marker-bar marker-color-${blueprint.colorName}`;
bar.style.transform = "translateX(" + (start - offset) + "px)";
bar.setAttribute("type", marker.name);
bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_BAR_WIDTH_MIN));
waterfall.appendChild(bar);
container.appendChild(waterfall);
},
/**
* Creates a dummy spacer as an empty marker.
*
* @param nsIDOMNode container
* The container node representing the marker.
*/
_buildMarkerSpacer: function(container) {
let sidebarSpacer = this._document.createElement("spacer");
sidebarSpacer.className = "waterfall-sidebar theme-sidebar";
sidebarSpacer.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
let waterfallSpacer = this._document.createElement("spacer");
waterfallSpacer.className = "waterfall-marker-item waterfall-background-ticks";
waterfallSpacer.setAttribute("flex", "1");
container.appendChild(sidebarSpacer);
container.appendChild(waterfallSpacer);
},
/**
* Creates the background displayed on the marker's waterfall.
*
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
*/
_drawWaterfallBackground: function(dataScale) {
if (!this._canvas || !this._ctx) {
this._canvas = this._document.createElementNS(HTML_NS, "canvas");
this._ctx = this._canvas.getContext("2d");
}
let canvas = this._canvas;
let ctx = this._ctx;
// Nuke the context.
let canvasWidth = canvas.width = this._waterfallWidth;
let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
// Start over.
let imageData = ctx.createImageData(canvasWidth, canvasHeight);
let pixelArray = imageData.data;
let buf = new ArrayBuffer(pixelArray.length);
let view8bit = new Uint8ClampedArray(buf);
let view32bit = new Uint32Array(buf);
// Build new millisecond tick lines...
let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
let tickInterval = this._findOptimalTickInterval({
ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
dataScale: dataScale
});
// Insert one pixel for each division on each scale.
for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
let increment = tickInterval * Math.pow(2, i);
for (let x = 0; x < canvasWidth; x += increment) {
let position = x | 0;
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
}
alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
}
// Flush the image data and cache the waterfall background.
pixelArray.set(view8bit);
ctx.putImageData(imageData, 0, 0);
this._document.mozSetImageElement("waterfall-background", canvas);
},
/**
* Finds the optimal tick interval between time markers in this timeline.
*
* @param number ticksMultiple
* @param number ticksSpacingMin
* @param number dataScale
* @return number
*/
_findOptimalTickInterval: function({ ticksMultiple, ticksSpacingMin, dataScale }) {
let timingStep = ticksMultiple;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > ticksSpacingMin) {
return dataScale;
}
while (true) {
let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < ticksSpacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
},
/**
* Returns true if this is document is in RTL mode.
* @return boolean
*/
_getRTL: function() {
let win = this._document.defaultView;
let doc = this._document.documentElement;
return win.getComputedStyle(doc, null).direction == "rtl";
}
};
/**
* Checks if a given marker is in the specified time range.
*
* @param object e
* The marker containing the { start, end } timestamps.
* @param number start
* The earliest allowed time.
* @param number end
* The latest allowed time.
* @return boolean
* True if the marker fits inside the specified time range.
*/
function isMarkerInRange(e, start, end) {
return (e.start >= start && e.end <= end) || // bounds inside
(e.start < start && e.end > end) || // bounds outside
(e.start < start && e.end >= start && e.end <= end) || // overlap start
(e.end > end && e.start >= start && e.start <= end); // overlap end
}
exports.Waterfall = Waterfall;
+3 -1
View File
@@ -15,11 +15,13 @@ EXTRA_JS_MODULES.devtools.performance += [
'modules/logic/recording-model.js',
'modules/logic/recording-utils.js',
'modules/logic/tree-model.js',
'modules/logic/waterfall-utils.js',
'modules/widgets/graphs.js',
'modules/widgets/marker-details.js',
'modules/widgets/marker-view.js',
'modules/widgets/markers-overview.js',
'modules/widgets/tree-view.js',
'modules/widgets/waterfall.js',
'modules/widgets/waterfall-ticks.js',
'panel.js'
]
@@ -25,12 +25,16 @@ loader.lazyRequireGetter(this, "RecordingModel",
"devtools/performance/recording-model", true);
loader.lazyRequireGetter(this, "GraphsController",
"devtools/performance/graphs", true);
loader.lazyRequireGetter(this, "Waterfall",
"devtools/performance/waterfall", true);
loader.lazyRequireGetter(this, "WaterfallHeader",
"devtools/performance/waterfall-ticks", true);
loader.lazyRequireGetter(this, "MarkerView",
"devtools/performance/marker-view", true);
loader.lazyRequireGetter(this, "MarkerDetails",
"devtools/performance/marker-details", true);
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");
loader.lazyRequireGetter(this, "WaterfallUtils",
"devtools/performance/waterfall-utils");
loader.lazyRequireGetter(this, "CallView",
"devtools/performance/tree-view", true);
loader.lazyRequireGetter(this, "ThreadNode",
+107 -61
View File
@@ -73,6 +73,8 @@
</popupset>
<hbox class="theme-body" flex="1">
<!-- Sidebar: controls and recording list -->
<vbox id="recordings-pane">
<toolbar id="recordings-toolbar"
class="devtools-toolbar">
@@ -92,43 +94,50 @@
<vbox id="recordings-list" flex="1"/>
</vbox>
<!-- Main panel content -->
<vbox id="performance-pane" flex="1">
<toolbar id="performance-toolbar" class="devtools-toolbar">
<hbox id="performance-toolbar-control-other" class="devtools-toolbarbutton-group">
<!-- Top toolbar controls -->
<toolbar id="performance-toolbar"
class="devtools-toolbar">
<hbox id="performance-toolbar-controls-other"
class="devtools-toolbarbutton-group">
<toolbarbutton id="filter-button"
class="devtools-toolbarbutton"
popup="performance-filter-menupopup"
tooltiptext="&profilerUI.options.filter.tooltiptext;"/>
</hbox>
<hbox id="performance-toolbar-controls-detail-views" class="devtools-toolbarbutton-group">
<hbox id="performance-toolbar-controls-detail-views"
class="devtools-toolbarbutton-group">
<toolbarbutton id="select-waterfall-view"
class="devtools-toolbarbutton devtools-button"
label="&profilerUI.toolbar.waterfall;"
label="Waterfall"
hidden="true"
data-view="waterfall" />
<toolbarbutton id="select-js-calltree-view"
class="devtools-toolbarbutton devtools-button"
label="&profilerUI.toolbar.js-calltree;"
label="Call Tree"
hidden="true"
data-view="js-calltree" />
<toolbarbutton id="select-js-flamegraph-view"
class="devtools-toolbarbutton devtools-button"
label="&profilerUI.toolbar.js-flamegraph;"
label="Flame Chart"
hidden="true"
data-view="js-flamegraph" />
<toolbarbutton id="select-memory-calltree-view"
class="devtools-toolbarbutton devtools-button"
label="&profilerUI.toolbar.memory-calltree1;"
label="Allocations Tree"
hidden="true"
data-view="memory-calltree" />
<toolbarbutton id="select-memory-flamegraph-view"
class="devtools-toolbarbutton devtools-button"
label="&profilerUI.toolbar.memory-flamegraph1;"
label="Allocations Chart"
hidden="true"
data-view="memory-flamegraph" />
</hbox>
<spacer flex="1"></spacer>
<hbox id="performance-toolbar-control-options" class="devtools-toolbarbutton-group">
<hbox id="performance-toolbar-controls-options"
class="devtools-toolbarbutton-group">
<toolbarbutton id="performance-options-button"
class="devtools-toolbarbutton devtools-option-toolbarbutton"
popup="performance-options-menupopup"
@@ -136,7 +145,10 @@
</hbox>
</toolbar>
<!-- Recording contents and general notice messages -->
<deck id="performance-view" flex="1">
<!-- "Empty" notice, shown when there's no recordings available -->
<hbox id="empty-notice"
class="notice-container"
align="center"
@@ -145,16 +157,25 @@
<hbox class="devtools-toolbarbutton-group"
pack="center">
<toolbarbutton class="devtools-toolbarbutton record-button"
label="&profilerUI.startRecording;" />
label="&profilerUI.startRecording;"
standalone="true"/>
</hbox>
</hbox>
<!-- Recording contents -->
<vbox id="performance-view-content" flex="1">
<!-- Overview graphs -->
<vbox id="overview-pane">
<hbox id="markers-overview"/>
<hbox id="memory-overview"/>
<hbox id="time-framerate"/>
</vbox>
<!-- Detail views and specific notice messages -->
<deck id="details-pane-container" flex="1">
<!-- "Loading" notice, shown when a recording is being loaded -->
<hbox id="loading-notice"
class="notice-container devtools-throbber"
align="center"
@@ -162,94 +183,113 @@
flex="1">
<label value="&profilerUI.loadingNotice;"/>
</hbox>
<hbox id="recording-notice"
<!-- "Recording" notice, shown when a recording is in progress -->
<vbox id="recording-notice"
class="notice-container"
align="center"
pack="center"
flex="1">
<vbox>
<hbox class="devtools-toolbarbutton-group"
pack="center">
<toolbarbutton class="devtools-toolbarbutton record-button"
label="&profilerUI.stopRecording;" />
</hbox>
<label class="realtime-disabled-message"
value="Realtime recording data disabled on non-multiprocess Firefox."/>
<label class="realtime-disabled-on-e10s-message"
value="Enable multiprocess Firefox in preferences for rendering recording data in realtime."/>
<label class="buffer-status-message"
tooltiptext="&profilerUI.bufferStatusTooltip;"/>
<label class="buffer-status-message-full"
value="&profilerUI.bufferStatusFull;"/>
</vbox>
</hbox>
<hbox id="console-recording-notice"
<hbox class="devtools-toolbarbutton-group"
pack="center">
<toolbarbutton class="devtools-toolbarbutton record-button"
label="&profilerUI.stopRecording;"
standalone="true"/>
</hbox>
<label class="realtime-disabled-message">
Realtime recording data disabled on non-multiprocess Firefox.
</label>
<label class="realtime-disabled-on-e10s-message">
Enable multiprocess Firefox in preferences for rendering recording data in realtime.
</label>
<label class="buffer-status-message"
tooltiptext="&profilerUI.bufferStatusTooltip;"/>
<label class="buffer-status-message-full"
value="&profilerUI.bufferStatusFull;"/>
</vbox>
<!-- "Console" notice, shown when a console recording is in progress -->
<vbox id="console-recording-notice"
class="notice-container"
align="center"
pack="center"
flex="1">
<vbox flex="1" align="center">
<hbox class="console-profile-recording-notice">
<label value="&profilerUI.console.recordingNoticeStart;" />
<label class="console-profile-command" />
<label value="&profilerUI.console.recordingNoticeEnd;" />
</hbox>
<hbox class="console-profile-stop-notice">
<label value="&profilerUI.console.stopCommandStart;" />
<label class="console-profile-command" />
<label value="&profilerUI.console.stopCommandEnd;" />
</hbox>
<label class="realtime-disabled-message"
value="Realtime recording data disabled on non-multiprocess Firefox."/>
<label class="realtime-disabled-on-e10s-message"
value="Enable multiprocess Firefox in preferences for rendering recording data in realtime."/>
<label class="buffer-status-message"
tooltiptext="&profilerUI.bufferStatusTooltip;"/>
<label class="buffer-status-message-full"
value="&profilerUI.bufferStatusFull;"/>
</vbox>
</hbox>
<hbox class="console-profile-recording-notice">
<label value="&profilerUI.console.recordingNoticeStart;" />
<label class="console-profile-command" />
<label value="&profilerUI.console.recordingNoticeEnd;" />
</hbox>
<hbox class="console-profile-stop-notice">
<label value="&profilerUI.console.stopCommandStart;" />
<label class="console-profile-command" />
<label value="&profilerUI.console.stopCommandEnd;" />
</hbox>
<label class="realtime-disabled-message">
Realtime recording data disabled on non-multiprocess Firefox.
</label>
<label class="realtime-disabled-on-e10s-message">
Enable multiprocess Firefox in preferences for rendering recording data in realtime.
</label>
<label class="buffer-status-message"
tooltiptext="&profilerUI.bufferStatusTooltip;"/>
<label class="buffer-status-message-full"
value="&profilerUI.bufferStatusFull;"/>
</vbox>
<!-- Detail views -->
<deck id="details-pane" flex="1">
<!-- Waterfall -->
<hbox id="waterfall-view" flex="1">
<vbox id="waterfall-breakdown" flex="1" />
<vbox flex="1">
<hbox id="waterfall-header" />
<vbox id="waterfall-breakdown" flex="1" />
</vbox>
<splitter class="devtools-side-splitter"/>
<vbox id="waterfall-details"
class="theme-sidebar"
width="150"
height="150"/>
class="theme-sidebar"/>
</hbox>
<!-- JS Tree and JIT view -->
<hbox id="js-profile-view" flex="1">
<vbox id="js-calltree-view" flex="1">
<hbox class="call-tree-headers-container">
<label class="plain call-tree-header"
type="duration"
crop="end"
value="&profilerUI.table.totalDuration2;"/>
value="&profilerUI.table.totalDuration2;"
tooltiptext="&profilerUI.table.totalDuration.tooltip;"/>
<label class="plain call-tree-header"
type="percentage"
crop="end"
value="&profilerUI.table.totalPercentage;"/>
value="&profilerUI.table.totalPercentage;"
tooltiptext="&profilerUI.table.totalPercentage.tooltip;"/>
<label class="plain call-tree-header"
type="self-duration"
crop="end"
value="&profilerUI.table.selfDuration2;"/>
value="&profilerUI.table.selfDuration2;"
tooltiptext="&profilerUI.table.selfDuration.tooltip;"/>
<label class="plain call-tree-header"
type="self-percentage"
crop="end"
value="&profilerUI.table.selfPercentage;"/>
value="&profilerUI.table.selfPercentage;"
tooltiptext="&profilerUI.table.selfPercentage.tooltip;"/>
<label class="plain call-tree-header"
type="samples"
crop="end"
value="&profilerUI.table.samples;"/>
value="&profilerUI.table.samples;"
tooltiptext="&profilerUI.table.samples.tooltip;"/>
<label class="plain call-tree-header"
type="function"
crop="end"
value="&profilerUI.table.function;"/>
value="&profilerUI.table.function;"
tooltiptext="&profilerUI.table.function.tooltip;"/>
</hbox>
<vbox class="call-tree-cells-container" flex="1"/>
</vbox>
<splitter id="js-call-tree-splitter" class="devtools-side-splitter"/>
<vbox id="jit-optimizations-view" hidden="true">
<toolbar id="jit-optimizations-toolbar" class="devtools-toolbar">
<hbox id="jit-optimizations-header">
@@ -263,19 +303,23 @@
</vbox>
</hbox>
<!-- JS FlameChart -->
<hbox id="js-flamegraph-view" flex="1">
</hbox>
<!-- Memory Tree -->
<vbox id="memory-calltree-view" flex="1">
<hbox class="call-tree-headers-container">
<label class="plain call-tree-header"
type="allocations"
crop="end"
value="&profilerUI.table.totalAlloc;"/>
value="&profilerUI.table.totalAlloc1;"
tooltiptext="&profilerUI.table.totalAlloc.tooltip;"/>
<label class="plain call-tree-header"
type="self-allocations"
crop="end"
value="&profilerUI.table.selfAlloc;"/>
value="&profilerUI.table.selfAlloc1;"
tooltiptext="&profilerUI.table.selfAlloc.tooltip;"/>
<label class="plain call-tree-header"
type="function"
crop="end"
@@ -284,8 +328,10 @@
<vbox class="call-tree-cells-container" flex="1"/>
</vbox>
<!-- Memory FlameChart -->
<hbox id="memory-flamegraph-view" flex="1">
</hbox>
</deck>
</deck>
</vbox>
@@ -21,7 +21,9 @@ function test() {
treeRoot.autoExpandDepth = 0;
treeRoot.attachTo(container);
let $$fun = node => container.querySelectorAll(".call-tree-cell[type=function] > " + node);
let $$ = node => container.querySelectorAll(node);
let $fun = (node, ancestor) => (ancestor || container).querySelector(".call-tree-cell[type=function] > " + node);
let $$fun = (node, ancestor) => (ancestor || container).querySelectorAll(".call-tree-cell[type=function] > " + node);
let $$dur = i => container.querySelectorAll(".call-tree-cell[type=duration]")[i];
let $$perc = i => container.querySelectorAll(".call-tree-cell[type=percentage]")[i];
let $$sampl = i => container.querySelectorAll(".call-tree-cell[type=samples]")[i];
@@ -39,13 +41,13 @@ function test() {
"The root's samples cell displays the correct value.");
is($$fun(".call-tree-name")[0].getAttribute("value"), "(root)",
"The root's function cell displays the correct name.");
is($$fun(".call-tree-url")[0].getAttribute("value"), "",
"The root's function cell displays the correct url.");
is($$fun(".call-tree-line")[0].getAttribute("value"), "",
"The root's function cell displays the correct line.");
is($$fun(".call-tree-host")[0].getAttribute("value"), "",
is($$fun(".call-tree-url")[0], null,
"The root's function cell displays no url.");
is($$fun(".call-tree-line")[0], null,
"The root's function cell displays no line.");
is($$fun(".call-tree-host")[0], null,
"The root's function cell displays the correct host.");
is($$fun(".call-tree-category")[0].getAttribute("value"), "",
is($$fun(".call-tree-category")[0], null,
"The root's function cell displays the correct category.");
treeRoot.expand();
@@ -63,17 +65,17 @@ function test() {
"The .A node's percentage cell displays the correct value.");
is($$sampl(1).getAttribute("value"), "4",
"The .A node's samples cell displays the correct value.");
is($$fun(".call-tree-name")[1].getAttribute("value"), "A",
is($fun(".call-tree-name", $$(".call-tree-item")[1]).getAttribute("value"), "A",
"The .A node's function cell displays the correct name.");
is($$fun(".call-tree-url")[1].getAttribute("value"), "baz",
is($fun(".call-tree-url", $$(".call-tree-item")[1]).getAttribute("value"), "baz",
"The .A node's function cell displays the correct url.");
ok($$fun(".call-tree-url")[1].getAttribute("tooltiptext").contains("http://foo/bar/baz"),
ok($fun(".call-tree-url", $$(".call-tree-item")[1]).getAttribute("tooltiptext").includes("http://foo/bar/baz"),
"The .A node's function cell displays the correct url tooltiptext.");
is($$fun(".call-tree-line")[1].getAttribute("value"), ":12",
is($fun(".call-tree-line", $$(".call-tree-item")[1]).getAttribute("value"), ":12",
"The .A node's function cell displays the correct line.");
is($$fun(".call-tree-host")[1].getAttribute("value"), "foo",
is($fun(".call-tree-host", $$(".call-tree-item")[1]).getAttribute("value"), "foo",
"The .A node's function cell displays the correct host.");
is($$fun(".call-tree-category")[1].getAttribute("value"), "Gecko",
is($fun(".call-tree-category", $$(".call-tree-item")[1]).getAttribute("value"), "Gecko",
"The .A node's function cell displays the correct category.");
let A = treeRoot.getChild();
@@ -92,17 +94,17 @@ function test() {
"The .A.B node's percentage cell displays the correct value.");
is($$sampl(2).getAttribute("value"), "3",
"The .A.B node's samples cell displays the correct value.");
is($$fun(".call-tree-name")[2].getAttribute("value"), "B",
is($fun(".call-tree-name", $$(".call-tree-item")[2]).getAttribute("value"), "B",
"The .A.B node's function cell displays the correct name.");
is($$fun(".call-tree-url")[2].getAttribute("value"), "baz",
is($fun(".call-tree-url", $$(".call-tree-item")[2]).getAttribute("value"), "baz",
"The .A.B node's function cell displays the correct url.");
ok($$fun(".call-tree-url")[2].getAttribute("tooltiptext").contains("http://foo/bar/baz"),
ok($fun(".call-tree-url", $$(".call-tree-item")[2]).getAttribute("tooltiptext").includes("http://foo/bar/baz"),
"The .A.B node's function cell displays the correct url tooltiptext.");
is($$fun(".call-tree-line")[2].getAttribute("value"), ":34",
is($fun(".call-tree-line", $$(".call-tree-item")[2]).getAttribute("value"), ":34",
"The .A.B node's function cell displays the correct line.");
is($$fun(".call-tree-host")[2].getAttribute("value"), "foo",
is($fun(".call-tree-host", $$(".call-tree-item")[2]).getAttribute("value"), "foo",
"The .A.B node's function cell displays the correct host.");
is($$fun(".call-tree-category")[2].getAttribute("value"), "Styles",
is($fun(".call-tree-category", $$(".call-tree-item")[2]).getAttribute("value"), "Styles",
"The .A.B node's function cell displays the correct category.");
is($$dur(3).getAttribute("value"), "7 ms",
@@ -111,17 +113,17 @@ function test() {
"The .A.E node's percentage cell displays the correct value.");
is($$sampl(3).getAttribute("value"), "1",
"The .A.E node's samples cell displays the correct value.");
is($$fun(".call-tree-name")[3].getAttribute("value"), "E",
is($fun(".call-tree-name", $$(".call-tree-item")[3]).getAttribute("value"), "E",
"The .A.E node's function cell displays the correct name.");
is($$fun(".call-tree-url")[3].getAttribute("value"), "baz",
is($fun(".call-tree-url", $$(".call-tree-item")[3]).getAttribute("value"), "baz",
"The .A.E node's function cell displays the correct url.");
ok($$fun(".call-tree-url")[3].getAttribute("tooltiptext").contains("http://foo/bar/baz"),
ok($fun(".call-tree-url", $$(".call-tree-item")[3]).getAttribute("tooltiptext").includes("http://foo/bar/baz"),
"The .A.E node's function cell displays the correct url tooltiptext.");
is($$fun(".call-tree-line")[3].getAttribute("value"), ":90",
is($fun(".call-tree-line", $$(".call-tree-item")[3]).getAttribute("value"), ":90",
"The .A.E node's function cell displays the correct line.");
is($$fun(".call-tree-host")[3].getAttribute("value"), "foo",
is($fun(".call-tree-host", $$(".call-tree-item")[3]).getAttribute("value"), "foo",
"The .A.E node's function cell displays the correct host.");
is($$fun(".call-tree-category")[3].getAttribute("value"), "GC",
is($fun(".call-tree-category", $$(".call-tree-item")[3]).getAttribute("value"), "GC",
"The .A.E node's function cell displays the correct category.");
finish();
@@ -26,6 +26,8 @@ function test() {
"The root node's 'category' attribute is correct.");
is(treeRoot.target.getAttribute("tooltiptext"), "",
"The root node's 'tooltiptext' attribute is correct.");
is(treeRoot.target.querySelector(".call-tree-category"), null,
"The root node's category label cell should be hidden.");
let A = treeRoot.getChild();
let B = A.getChild();
@@ -35,7 +37,7 @@ function test() {
"The .A.B.D node's 'origin' attribute is correct.");
is(D.target.getAttribute("category"), "gc",
"The .A.B.D node's 'category' attribute is correct.");
is(D.target.getAttribute("tooltiptext"), "D (http://foo/bar/baz:78)",
is(D.target.getAttribute("tooltiptext"), "D (http://foo/bar/baz:78:1337)",
"The .A.B.D node's 'tooltiptext' attribute is correct.");
ok(!A.target.querySelector(".call-tree-category").hidden,
"The .A.B.D node's category label cell should not be hidden.");
@@ -70,7 +72,7 @@ function test() {
is(functionCell.childNodes[4].className, "plain call-tree-column",
"The fifth node displayed for function cells is correct.");
is(functionCell.childNodes[5].className, "plain call-tree-host",
"The fifth node displayed for function cells is correct.");
"The sixth node displayed for function cells is correct.");
is(functionCell.childNodes[6].tagName, "spacer",
"The seventh node displayed for function cells is correct.");
is(functionCell.childNodes[7].className, "plain call-tree-category",
@@ -93,7 +95,7 @@ let gThread = synthesizeProfileForTest([{
{ category: CATEGORY_MASK('other'), location: "(root)" },
{ category: CATEGORY_MASK('other'), location: "A (http://foo/bar/baz:12)" },
{ category: CATEGORY_MASK('css'), location: "B (http://foo/bar/baz:34)" },
{ category: CATEGORY_MASK('gc', 1), location: "D (http://foo/bar/baz:78)" }
{ category: CATEGORY_MASK('gc', 1), location: "D (http://foo/bar/baz:78:1337)" }
]
}, {
time: 5 + 1 + 2,
@@ -101,7 +103,7 @@ let gThread = synthesizeProfileForTest([{
{ category: CATEGORY_MASK('other'), location: "(root)" },
{ category: CATEGORY_MASK('other'), location: "A (http://foo/bar/baz:12)" },
{ category: CATEGORY_MASK('css'), location: "B (http://foo/bar/baz:34)" },
{ category: CATEGORY_MASK('gc', 1), location: "D (http://foo/bar/baz:78)" }
{ category: CATEGORY_MASK('gc', 1), location: "D (http://foo/bar/baz:78:1337)" }
]
}, {
time: 5 + 1 + 2 + 7,
@@ -19,13 +19,13 @@ function test() {
treeRoot.attachTo(container);
let categories = container.querySelectorAll(".call-tree-category");
is(categories.length, 7,
is(categories.length, 5,
"The call tree displays a correct number of categories.");
ok(!container.hasAttribute("categories-hidden"),
"All categories should be visible in the tree.");
treeRoot.toggleCategories(false);
is(categories.length, 7,
is(categories.length, 5,
"The call tree displays the same number of categories.");
ok(container.hasAttribute("categories-hidden"),
"All categories should now be hidden in the tree.");
@@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const MARKER_DETAILS_WIDTH = 300;
const MARKER_DETAILS_WIDTH = 200;
/**
* Waterfall view containing the timeline markers, controlled by DetailsView.
@@ -26,24 +26,21 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
initialize: function () {
DetailsSubview.initialize.call(this);
// TODO bug 1167093 save the previously set width, and ensure minimum width
$("#waterfall-details").setAttribute("width", MARKER_DETAILS_WIDTH);
this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#waterfall-view"));
this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
this._onMarkerSelected = this._onMarkerSelected.bind(this);
this._onResize = this._onResize.bind(this);
this._onViewSource = this._onViewSource.bind(this);
this.waterfall.on("selected", this._onMarkerSelected);
this.waterfall.on("unselected", this._onMarkerSelected);
this.headerContainer = $("#waterfall-header");
this.breakdownContainer = $("#waterfall-breakdown");
this.detailsContainer = $("#waterfall-details");
this.detailsSplitter = $("#waterfall-view > splitter");
this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
this.details.on("resize", this._onResize);
this.details.on("view-source", this._onViewSource);
let blueprint = PerformanceController.getTimelineBlueprint();
this.waterfall.setBlueprint(blueprint);
this.waterfall.recalculateBounds();
// TODO bug 1167093 save the previously set width, and ensure minimum width
this.details.width = MARKER_DETAILS_WIDTH;
},
/**
@@ -52,8 +49,6 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
destroy: function () {
DetailsSubview.destroy.call(this);
this.waterfall.off("selected", this._onMarkerSelected);
this.waterfall.off("unselected", this._onMarkerSelected);
this.details.off("resize", this._onResize);
this.details.off("view-source", this._onViewSource);
},
@@ -69,7 +64,9 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
let startTime = interval.startTime || 0;
let endTime = interval.endTime || recording.getDuration();
let markers = recording.getMarkers();
this.waterfall.setData({ markers, interval: { startTime, endTime } });
let rootMarkerNode = this._prepareWaterfallTree(markers);
this._populateWaterfallTree(rootMarkerNode, { startTime, endTime });
this.emit(EVENTS.WATERFALL_RENDERED);
},
@@ -79,11 +76,6 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
*/
_onMarkerSelected: function (event, marker) {
let recording = PerformanceController.getCurrentRecording();
// Race condition in tests due to lazy rendering of markers in the
// waterfall? intermittent bug 1157523
if (!recording) {
return;
}
let frames = recording.getFrames();
if (event === "selected") {
@@ -98,7 +90,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
* Called when the marker details view is resized.
*/
_onResize: function () {
this.waterfall.recalculateBounds();
this._markersRoot.recalculateBounds();
this.render();
},
@@ -107,7 +99,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
*/
_onObservedPrefChange: function(_, prefName) {
let blueprint = PerformanceController.getTimelineBlueprint();
this.waterfall.setBlueprint(blueprint);
this._markersRoot.blueprint = blueprint;
},
/**
@@ -117,5 +109,50 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
gToolbox.viewSourceInDebugger(file, line);
},
/**
* Called when the recording is stopped and prepares data to
* populate the waterfall tree.
*/
_prepareWaterfallTree: function(markers) {
let rootMarkerNode = WaterfallUtils.makeEmptyMarkerNode("(root)");
WaterfallUtils.collapseMarkersIntoNode({
markerNode: rootMarkerNode,
markersList: markers
});
return rootMarkerNode;
},
/**
* Renders the waterfall tree.
*/
_populateWaterfallTree: function(rootMarkerNode, interval) {
let root = new MarkerView({
marker: rootMarkerNode,
// The root node is irrelevant in a waterfall tree.
hidden: true,
// The waterfall tree should not expand by default.
autoExpandDepth: 0
});
let header = new WaterfallHeader(root);
this._markersRoot = root;
this._waterfallHeader = header;
let blueprint = PerformanceController.getTimelineBlueprint();
root.blueprint = blueprint;
root.interval = interval;
root.on("selected", this._onMarkerSelected);
root.on("unselected", this._onMarkerSelected);
this.breakdownContainer.innerHTML = "";
root.attachTo(this.breakdownContainer);
this.headerContainer.innerHTML = "";
header.attachTo(this.headerContainer);
},
toString: () => "[object WaterfallView]"
});
+32 -21
View File
@@ -33,8 +33,8 @@ XPCOMUtils.defineLazyGetter(this, "events", () => {
});
for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
"ConsoleAPIListener", "JSTermHelpers", "JSPropertyProvider",
"ConsoleReflowListener"]) {
"ConsoleAPIListener", "addWebConsoleCommands", "JSPropertyProvider",
"ConsoleReflowListener", "CONSOLE_WORKER_IDS"]) {
Object.defineProperty(this, name, {
get: function(prop) {
if (prop == "WebConsoleUtils") {
@@ -297,11 +297,11 @@ WebConsoleActor.prototype =
consoleReflowListener: null,
/**
* The JSTerm Helpers names cache.
* The Web Console Commands names cache.
* @private
* @type array
*/
_jstermHelpersCache: null,
_webConsoleCommandsCache: null,
actorPrefix: "console",
@@ -362,7 +362,7 @@ WebConsoleActor.prototype =
}
this._actorPool = null;
this._jstermHelpersCache = null;
this._webConsoleCommandsCache = null;
this._lastConsoleInputEvaluation = null;
this._evalWindow = null;
this._netEvents.clear();
@@ -750,8 +750,6 @@ WebConsoleActor.prototype =
}
}
messages.sort(function(a, b) { return a.timeStamp - b.timeStamp; });
return {
from: this.actorID,
messages: messages,
@@ -897,14 +895,16 @@ WebConsoleActor.prototype =
// helper functions.
let lastNonAlphaIsDot = /[.][a-zA-Z0-9$]*$/.test(reqText);
if (!lastNonAlphaIsDot) {
if (!this._jstermHelpersCache) {
if (!this._webConsoleCommandsCache) {
let helpers = {
sandbox: Object.create(null)
};
JSTermHelpers(helpers);
this._jstermHelpersCache = Object.getOwnPropertyNames(helpers.sandbox);
addWebConsoleCommands(helpers);
this._webConsoleCommandsCache =
Object.getOwnPropertyNames(helpers.sandbox);
}
matches = matches.concat(this._jstermHelpersCache.filter(n => n.startsWith(result.matchProp)));
matches = matches.concat(this._webConsoleCommandsCache
.filter(n => n.startsWith(result.matchProp)));
}
return {
@@ -926,6 +926,10 @@ WebConsoleActor.prototype =
.getService(Ci.nsIConsoleAPIStorage);
ConsoleAPIStorage.clearEvents(windowId);
CONSOLE_WORKER_IDS.forEach((aId) => {
ConsoleAPIStorage.clearEvents(aId);
});
if (this.parentActor.isRootActor) {
Services.console.logStringMessage(null); // for the Error Console
Services.console.reset();
@@ -981,13 +985,13 @@ WebConsoleActor.prototype =
* @private
* @param object aDebuggerGlobal
* A Debugger.Object that wraps a content global. This is used for the
* JSTerm helpers.
* Web Console Commands.
* @return object
* The same object as |this|, but with an added |sandbox| property.
* The sandbox holds methods and properties that can be used as
* bindings during JS evaluation.
*/
_getJSTermHelpers: function WCA__getJSTermHelpers(aDebuggerGlobal)
_getWebConsoleCommands: function(aDebuggerGlobal)
{
let helpers = {
window: this.evalWindow,
@@ -998,7 +1002,7 @@ WebConsoleActor.prototype =
helperResult: null,
consoleActor: this,
};
JSTermHelpers(helpers);
addWebConsoleCommands(helpers);
let evalWindow = this.evalWindow;
function maybeExport(obj, name) {
@@ -1052,9 +1056,9 @@ WebConsoleActor.prototype =
*
* The Debugger.Frame comes from the jsdebugger's Debugger instance, which
* is different from the Web Console's Debugger instance. This means that
* for evaluation to work, we need to create a new instance for the jsterm
* helpers - they need to be Debugger.Objects coming from the jsdebugger's
* Debugger instance.
* for evaluation to work, we need to create a new instance for the Web
* Console Commands helpers - they need to be Debugger.Objects coming from the
* jsdebugger's Debugger instance.
*
* When |bindObjectActor| is used objects can come from different iframes,
* from different domains. To avoid permission-related errors when objects
@@ -1084,7 +1088,8 @@ WebConsoleActor.prototype =
* - window: the Debugger.Object for the global where the string was
* evaluated.
* - result: the result of the evaluation.
* - helperResult: any result coming from a JSTerm helper function.
* - helperResult: any result coming from a Web Console commands
* function.
* - url: the url to evaluate the script as. Defaults to
* "debugger eval code".
*/
@@ -1140,8 +1145,8 @@ WebConsoleActor.prototype =
}
}
// Get the JSTerm helpers for the given debugger window.
let helpers = this._getJSTermHelpers(dbgWindow);
// Get the Web Console commands for the given debugger window.
let helpers = this._getWebConsoleCommands(dbgWindow);
let bindings = helpers.sandbox;
if (bindSelf) {
bindings._self = bindSelf;
@@ -1155,7 +1160,8 @@ WebConsoleActor.prototype =
}
// Check if the Debugger.Frame or Debugger.Object for the global include
// $ or $$. We will not overwrite these functions with the jsterm helpers.
// $ or $$. We will not overwrite these functions with the Web Console
// commands.
let found$ = false, found$$ = false;
if (frame) {
let env = frame.environment;
@@ -1443,6 +1449,10 @@ WebConsoleActor.prototype =
function WCA_prepareConsoleMessageForRemote(aMessage)
{
let result = WebConsoleUtils.cloneObject(aMessage);
result.workerType = CONSOLE_WORKER_IDS.indexOf(result.innerID) == -1
? 'none' : result.innerID;
delete result.wrappedJSObject;
delete result.ID;
delete result.innerID;
@@ -1609,6 +1619,7 @@ NetworkEventActor.prototype =
return {
actor: this.actorID,
startedDateTime: this._startedDateTime,
timeStamp: Date.parse(this._startedDateTime),
url: this._request.url,
method: this._request.method,
isXHR: this._isXHR,
@@ -5,12 +5,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cu = Components.utils;
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/ViewHelpers.jsm");
Cu.import("resource://gre/modules/devtools/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://gre/modules/devtools/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
this.EXPORTED_SYMBOLS = ["AbstractTreeItem"];
@@ -117,13 +121,12 @@ function AbstractTreeItem({ parent, level }) {
this._parentItem = parent;
this._level = level || 0;
this._childTreeItems = [];
this._onArrowClick = this._onArrowClick.bind(this);
this._onClick = this._onClick.bind(this);
this._onDoubleClick = this._onDoubleClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._onFocus = this._onFocus.bind(this);
EventEmitter.decorate(this);
// Events are always propagated through the root item. Decorating every
// tree item as an event emitter is a very costly operation.
if (this == this._rootItem) {
EventEmitter.decorate(this);
}
}
AbstractTreeItem.prototype = {
@@ -150,7 +153,8 @@ AbstractTreeItem.prototype = {
* @return nsIDOMNode
*/
_displaySelf: function(document, arrowNode) {
throw "This method needs to be implemented by inheriting classes.";
throw new Error(
"The `_displaySelf` method needs to be implemented by inheriting classes.");
},
/**
@@ -162,7 +166,16 @@ AbstractTreeItem.prototype = {
* @param array:AbstractTreeItem children
*/
_populateSelf: function(children) {
throw "This method needs to be implemented by inheriting classes.";
throw new Error(
"The `_populateSelf` method needs to be implemented by inheriting classes.");
},
/**
* Gets the this tree's owner document.
* @return Document
*/
get document() {
return this._containerNode.ownerDocument;
},
/**
@@ -221,18 +234,36 @@ AbstractTreeItem.prototype = {
return this._expanded;
},
/**
* Gets the bounds for this tree's container without flushing.
* @return object
*/
get bounds() {
let win = this.document.defaultView;
let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
return utils.getBoundsWithoutFlushing(this._containerNode);
},
/**
* Creates and appends this tree item to the specified parent element.
*
* @param nsIDOMNode containerNode
* The parent element for this tree item (and every other tree item).
* @param nsIDOMNode beforeNode
* The child element which should succeed this tree item.
* @param nsIDOMNode fragmentNode [optional]
* An optional document fragment temporarily holding this tree item in
* the current batch. Defaults to the `containerNode`.
* @param nsIDOMNode beforeNode [optional]
* An optional child element which should succeed this tree item.
*/
attachTo: function(containerNode, beforeNode = null) {
attachTo: function(containerNode, fragmentNode = containerNode, beforeNode = null) {
this._containerNode = containerNode;
this._constructTargetNode();
containerNode.insertBefore(this._targetNode, beforeNode);
if (beforeNode) {
fragmentNode.insertBefore(this._targetNode, beforeNode);
} else {
fragmentNode.appendChild(this._targetNode);
}
if (this._level < this.autoExpandDepth) {
this.expand();
@@ -265,6 +296,7 @@ AbstractTreeItem.prototype = {
}
this._expanded = true;
this._arrowNode.setAttribute("open", "");
this._targetNode.setAttribute("expanded", "");
this._toggleChildren(true);
this._rootItem.emit("expand", this);
},
@@ -278,6 +310,7 @@ AbstractTreeItem.prototype = {
}
this._expanded = false;
this._arrowNode.removeAttribute("open");
this._targetNode.removeAttribute("expanded", "");
this._toggleChildren(false);
this._rootItem.emit("collapse", this);
},
@@ -315,17 +348,16 @@ AbstractTreeItem.prototype = {
* Shows all children of this item in the tree.
*/
_showChildren: function() {
let childTreeItems = this._childTreeItems;
let expandedChildTreeItems = childTreeItems.filter(e => e._expanded);
let nextNode = this._getSiblingAtDelta(1);
// First append the child items, and afterwards append any descendants.
// Otherwise, the tree will become garbled and nodes will intertwine.
for (let item of childTreeItems) {
item.attachTo(this._containerNode, nextNode);
// If this is the root item and we're not expanding any child nodes,
// it is safe to append everything at once.
if (this == this._rootItem && this.autoExpandDepth == 0) {
this._appendChildrenBatch();
}
for (let item of expandedChildTreeItems) {
item._showChildren();
// Otherwise, append the child items and their descendants successively;
// if not, the tree will become garbled and nodes will intertwine,
// since all the tree items are sharing a single container node.
else {
this._appendChildrenSuccessive();
}
},
@@ -339,6 +371,40 @@ AbstractTreeItem.prototype = {
}
},
/**
* Appends all children in a single batch.
* This only works properly for root nodes when no child nodes will expand.
*/
_appendChildrenBatch: function() {
if (this._fragment === undefined) {
this._fragment = this.document.createDocumentFragment();
}
let childTreeItems = this._childTreeItems;
for (let i = 0, len = childTreeItems.length; i < len; i++) {
childTreeItems[i].attachTo(this._containerNode, this._fragment);
}
this._containerNode.appendChild(this._fragment);
},
/**
* Appends all children successively.
*/
_appendChildrenSuccessive: function() {
let childTreeItems = this._childTreeItems;
let expandedChildTreeItems = childTreeItems.filter(e => e._expanded);
let nextNode = this._getSiblingAtDelta(1);
for (let i = 0, len = childTreeItems.length; i < len; i++) {
childTreeItems[i].attachTo(this._containerNode, undefined, nextNode);
}
for (let i = 0, len = expandedChildTreeItems.length; i < len; i++) {
expandedChildTreeItems[i]._showChildren();
}
},
/**
* Constructs and stores the target node displaying this tree item.
*/
@@ -346,7 +412,14 @@ AbstractTreeItem.prototype = {
if (this._constructed) {
return;
}
let document = this._containerNode.ownerDocument;
this._onArrowClick = this._onArrowClick.bind(this);
this._onClick = this._onClick.bind(this);
this._onDoubleClick = this._onDoubleClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._onFocus = this._onFocus.bind(this);
this._onBlur = this._onBlur.bind(this);
let document = this.document;
let arrowNode = this._arrowNode = document.createElement("hbox");
arrowNode.className = "arrow theme-twisty";
@@ -359,6 +432,7 @@ AbstractTreeItem.prototype = {
targetNode.addEventListener("dblclick", this._onDoubleClick);
targetNode.addEventListener("keypress", this._onKeyPress);
targetNode.addEventListener("focus", this._onFocus);
targetNode.addEventListener("blur", this._onBlur);
this._constructed = true;
},
@@ -434,7 +508,6 @@ AbstractTreeItem.prototype = {
if (!e.target.classList.contains("arrow")) {
this._onArrowClick(e);
}
this.focus();
},
@@ -477,5 +550,12 @@ AbstractTreeItem.prototype = {
*/
_onFocus: function(e) {
this._rootItem.emit("focus", this);
},
/**
* Handler for the "blur" event on the element displaying this tree item.
*/
_onBlur: function(e) {
this._rootItem.emit("blur", this);
}
};
@@ -1991,9 +1991,14 @@ AbstractCanvasGraph.createIframe = function(url, parent, callback) {
callback(iframe);
});
// Setting 100% width on the frame and flex on the parent allows the graph
// to properly shrink when the window is resized to be smaller.
iframe.setAttribute("frameborder", "0");
iframe.style.width = "100%";
iframe.style.minWidth = "50px";
iframe.src = url;
parent.style.display = "flex";
parent.appendChild(iframe);
};
+1 -1
View File
@@ -33,7 +33,7 @@
background-position: -48px center;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#eyedropper-button {
background-image: url("chrome://global/skin/devtools/command-eyedropper@2x.png");
}
@@ -42,7 +42,7 @@
position: relative;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.breakpoint {
background-image: url("chrome://global/skin/devtools/editor-breakpoint@2x.png");
}
@@ -56,7 +56,7 @@
background-image: url("chrome://global/skin/devtools/editor-debug-location.png");
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.debugLocation {
background-image: url("chrome://global/skin/devtools/editor-debug-location@2x.png");
}
@@ -68,7 +68,7 @@
url("chrome://global/skin/devtools/editor-breakpoint.png");
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.breakpoint.debugLocation {
background-image:
url("chrome://global/skin/devtools/editor-debug-location@2x.png"),
+144 -4
View File
@@ -8,6 +8,7 @@
const {Cc, Ci, Cu} = require("chrome");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const EventEmitter = require("devtools/toolkit/event-emitter");
loader.lazyImporter(this, "LongStringClient", "resource://gre/modules/devtools/dbg-client.jsm");
@@ -28,11 +29,17 @@ function WebConsoleClient(aDebuggerClient, aResponse)
this._longStrings = {};
this.traits = aResponse.traits || {};
this.events = [];
this._networkRequests = new Map();
this.pendingEvaluationResults = new Map();
this.onEvaluationResult = this.onEvaluationResult.bind(this);
this.onNetworkEvent = this._onNetworkEvent.bind(this);
this.onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
this._client.addListener("evaluationResult", this.onEvaluationResult);
this._client.addListener("networkEvent", this.onNetworkEvent);
this._client.addListener("networkEventUpdate", this.onNetworkEventUpdate);
EventEmitter.decorate(this);
}
exports.WebConsoleClient = WebConsoleClient;
@@ -41,24 +48,145 @@ WebConsoleClient.prototype = {
_longStrings: null,
traits: null,
/**
* Holds the network requests currently displayed by the Web Console. Each key
* represents the connection ID and the value is network request information.
* @private
* @type object
*/
_networkRequests: null,
getNetworkRequest(actorId) {
return this._networkRequests.get(actorId);
},
hasNetworkRequest(actorId) {
return this._networkRequests.has(actorId);
},
removeNetworkRequest(actorId) {
this._networkRequests.delete(actorId);
},
getNetworkEvents() {
return this._networkRequests.values();
},
get actor() { return this._actor; },
/**
* The "networkEvent" message type handler. We redirect any message to
* the UI for displaying.
*
* @private
* @param string type
* Message type.
* @param object packet
* The message received from the server.
*/
_onNetworkEvent: function (type, packet)
{
if (packet.from == this._actor) {
let actor = packet.eventActor;
let networkInfo = {
_type: "NetworkEvent",
timeStamp: actor.timeStamp,
node: null,
actor: actor.actor,
discardRequestBody: true,
discardResponseBody: true,
startedDateTime: actor.startedDateTime,
request: {
url: actor.url,
method: actor.method,
},
isXHR: actor.isXHR,
response: {},
timings: {},
updates: [], // track the list of network event updates
private: actor.private,
fromCache: actor.fromCache
};
this._networkRequests.set(actor.actor, networkInfo);
this.emit("networkEvent", networkInfo);
}
},
/**
* The "networkEventUpdate" message type handler. We redirect any message to
* the UI for displaying.
*
* @private
* @param string type
* Message type.
* @param object packet
* The message received from the server.
*/
_onNetworkEventUpdate: function (type, packet)
{
let networkInfo = this.getNetworkRequest(packet.from);
if (!networkInfo) {
return;
}
networkInfo.updates.push(packet.updateType);
switch (packet.updateType) {
case "requestHeaders":
networkInfo.request.headersSize = packet.headersSize;
break;
case "requestPostData":
networkInfo.discardRequestBody = packet.discardRequestBody;
networkInfo.request.bodySize = packet.dataSize;
break;
case "responseStart":
networkInfo.response.httpVersion = packet.response.httpVersion;
networkInfo.response.status = packet.response.status;
networkInfo.response.statusText = packet.response.statusText;
networkInfo.response.headersSize = packet.response.headersSize;
networkInfo.response.remoteAddress = packet.response.remoteAddress;
networkInfo.response.remotePort = packet.response.remotePort;
networkInfo.discardResponseBody = packet.response.discardResponseBody;
break;
case "responseContent":
networkInfo.response.content = {
mimeType: packet.mimeType,
};
networkInfo.response.bodySize = packet.contentSize;
networkInfo.response.transferredSize = packet.transferredSize;
networkInfo.discardResponseBody = packet.discardResponseBody;
break;
case "eventTimings":
networkInfo.totalTime = packet.totalTime;
break;
case "securityInfo":
networkInfo.securityInfo = packet.state;
break;
}
this.emit("networkEventUpdate", {
packet: packet,
networkInfo
});
},
/**
* Retrieve the cached messages from the server.
*
* @see this.CACHED_MESSAGES
* @param array aTypes
* @param array types
* The array of message types you want from the server. See
* this.CACHED_MESSAGES for known types.
* @param function aOnResponse
* The function invoked when the response is received.
*/
getCachedMessages: function WCC_getCachedMessages(aTypes, aOnResponse)
getCachedMessages: function WCC_getCachedMessages(types, aOnResponse)
{
let packet = {
to: this._actor,
type: "getCachedMessages",
messageTypes: aTypes,
messageTypes: types,
};
this._client.request(packet, aOnResponse);
},
@@ -154,7 +282,11 @@ WebConsoleClient.prototype = {
};
this._client.request(packet, response => {
this.pendingEvaluationResults.set(response.resultID, aOnResponse);
// Null check this in case the client has been detached while waiting
// for a response.
if (this.pendingEvaluationResults) {
this.pendingEvaluationResults.set(response.resultID, aOnResponse);
}
});
},
@@ -469,10 +601,18 @@ WebConsoleClient.prototype = {
detach: function WCC_detach(aOnResponse)
{
this._client.removeListener("evaluationResult", this.onEvaluationResult);
this._client.removeListener("networkEvent", this.onNetworkEvent);
this._client.removeListener("networkEventUpdate", this.onNetworkEventUpdate);
this.stopListeners(null, aOnResponse);
this._longStrings = null;
this._client = null;
this.pendingEvaluationResults.clear();
this.pendingEvaluationResults = null;
this.clearNetworkRequests();
this._networkRequests = null;
},
clearNetworkRequests: function () {
this._networkRequests.clear();
}
};
@@ -0,0 +1,105 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Test that console command input is persisted across toolbox loads.
// See Bug 943306.
"use strict";
const TEST_URI = "data:text/html;charset=utf-8,Web Console test for persisting history - bug 943306";
const INPUT_HISTORY_COUNT = 10;
let test = asyncTest(function* () {
info ("Setting custom input history pref to " + INPUT_HISTORY_COUNT);
Services.prefs.setIntPref("devtools.webconsole.inputHistoryCount", INPUT_HISTORY_COUNT);
// First tab: run a bunch of commands and then make sure that you can
// navigate through their history.
yield loadTab(TEST_URI);
let hud1 = yield openConsole();
is (JSON.stringify(hud1.jsterm.history), "[]", "No history on first tab initially");
yield populateInputHistory(hud1);
is (JSON.stringify(hud1.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]',
"First tab has populated history");
// Second tab: Just make sure that you can navigate through the history
// generated by the first tab.
yield loadTab(TEST_URI);
let hud2 = yield openConsole();
is (JSON.stringify(hud2.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]',
"Second tab has populated history");
yield testNaviatingHistoryInUI(hud2);
is (JSON.stringify(hud2.jsterm.history), '["0","1","2","3","4","5","6","7","8","9",""]',
"An empty entry has been added in the second tab due to history perusal");
// Third tab: Should have the same history as first tab, but if we run a
// command, then the history of the first and second shouldn't be affected
yield loadTab(TEST_URI);
let hud3 = yield openConsole();
is (JSON.stringify(hud3.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]',
"Third tab has populated history");
// Set input value separately from execute so UP arrow accurately navigates history.
hud3.jsterm.setInputValue('"hello from third tab"');
hud3.jsterm.execute();
is (JSON.stringify(hud1.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]',
"First tab history hasn't changed due to command in third tab");
is (JSON.stringify(hud2.jsterm.history), '["0","1","2","3","4","5","6","7","8","9",""]',
"Second tab history hasn't changed due to command in third tab");
is (JSON.stringify(hud3.jsterm.history), '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
"Third tab has updated history (and purged the first result) after running a command");
// Fourth tab: Should have the latest command from the third tab, followed
// by the rest of the history from the first tab.
yield loadTab(TEST_URI);
let hud4 = yield openConsole();
is (JSON.stringify(hud4.jsterm.history), '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
"Fourth tab has most recent history");
yield hud4.jsterm.clearHistory();
is (JSON.stringify(hud4.jsterm.history), '[]',
"Clearing history for a tab works");
yield loadTab(TEST_URI);
let hud5 = yield openConsole();
is (JSON.stringify(hud5.jsterm.history), '[]',
"Clearing history carries over to a new tab");
info ("Clearing custom input history pref");
Services.prefs.clearUserPref("devtools.webconsole.inputHistoryCount");
});
/**
* Populate the history by running the following commands:
* [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
*/
function* populateInputHistory(hud) {
let jsterm = hud.jsterm;
let {inputNode} = jsterm;
for (let i = 0; i < INPUT_HISTORY_COUNT; i++) {
// Set input value separately from execute so UP arrow accurately navigates history.
jsterm.setInputValue(i);
jsterm.execute();
}
}
/**
* Check pressing up results in history traversal like:
* [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
*/
function* testNaviatingHistoryInUI(hud) {
let jsterm = hud.jsterm;
let {inputNode} = jsterm;
inputNode.focus();
// Count backwards from original input and make sure that pressing up
// restores this.
for (let i = INPUT_HISTORY_COUNT - 1; i >= 0; i--) {
EventUtils.synthesizeKey("VK_UP", {});
is(inputNode.value, i, "Pressing up restores last input");
}
}
@@ -0,0 +1,71 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const TEST_URI = "data:text/html;charset=utf8,Test that the netmonitor " +
"displays requests that have been recorded in the " +
"web console, even if the netmonitor hadn't opened yet.";
const TEST_FILE = "test-network-request.html";
const TEST_PATH = "http://example.com/browser/toolkit/devtools/webconsole/test/" +
TEST_FILE;
const NET_PREF = "devtools.webconsole.filter.networkinfo";
Services.prefs.setBoolPref(NET_PREF, true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref(NET_PREF);
});
add_task(function* () {
let { tab, browser } = yield loadTab(TEST_URI);
// Test that the request appears in the console.
let hud = yield openConsole();
info("Web console is open");
yield loadDocument(browser);
info("Document loaded.");
yield waitForMessages({
webconsole: hud,
messages: [
{
name: "network message",
text: TEST_FILE,
category: CATEGORY_NETWORK,
severity: SEVERITY_LOG
}
]
});
// Test that the request appears in the network panel.
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
info("Network panel is open.");
testNetmonitor(toolbox);
});
function loadDocument(browser) {
let deferred = promise.defer();
browser.addEventListener("load", function onLoad() {
browser.removeEventListener("load", onLoad, true);
deferred.resolve();
}, true);
content.location = TEST_PATH;
return deferred.promise;
}
function testNetmonitor(toolbox) {
let monitor = toolbox.getCurrentPanel();
let { RequestsMenu } = monitor.panelWin.NetMonitorView;
is(RequestsMenu.itemCount, 1, "Network request appears in the network panel");
let item = RequestsMenu.getItemAtIndex(0);
is(item.attachment.method, "GET", "The attached method is correct.");
is(item.attachment.url, TEST_PATH, "The attached url is correct.");
}
@@ -0,0 +1,142 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const prefs = {
"net": [
"network",
"netwarn",
"netxhr",
"networkinfo"
],
"css": [
"csserror",
"cssparser",
"csslog"
],
"js": [
"exception",
"jswarn",
"jslog",
],
"logging": [
"error",
"warn",
"info",
"log",
"serviceworkers",
"sharedworkers",
"windowlessworkers"
]
};
let test = asyncTest(function* () {
// Set all prefs to true
for (let category in prefs) {
prefs[category].forEach(function(pref) {
Services.prefs.setBoolPref("devtools.webconsole.filter." + pref, true);
});
}
yield loadTab("about:blank");
let hud = yield openConsole();
let hud2 = yield onConsoleOpen(hud);
let hud3 = yield onConsoleReopen1(hud2);
yield onConsoleReopen2(hud3);
// Clear prefs
for (let category in prefs) {
prefs[category].forEach(function(pref) {
Services.prefs.clearUserPref("devtools.webconsole.filter." + pref);
});
}
});
function onConsoleOpen(hud) {
let deferred = promise.defer();
let hudBox = hud.ui.rootElement;
// Check if the filters menuitems exists and are checked
for (let category in prefs) {
let button = hudBox.querySelector(".webconsole-filter-button[category=\""
+ category + "\"]");
ok(isChecked(button), "main button for " + category + " category is checked");
prefs[category].forEach(function(pref) {
let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]");
ok(isChecked(menuitem), "menuitem for " + pref + " is checked");
});
}
// Set all prefs to false
for (let category in prefs) {
prefs[category].forEach(function(pref) {
hud.setFilterState(pref, false);
});
}
//Re-init the console
closeConsole().then(() => {
openConsole().then(deferred.resolve);
});
return deferred.promise;
}
function onConsoleReopen1(hud) {
info("testing after reopening once");
let deferred = promise.defer();
let hudBox = hud.ui.rootElement;
// Check if the filter button and menuitems are unchecked
for (let category in prefs) {
let button = hudBox.querySelector(".webconsole-filter-button[category=\""
+ category + "\"]");
ok(isUnchecked(button), "main button for " + category + " category is not checked");
prefs[category].forEach(function(pref) {
let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]");
ok(isUnchecked(menuitem), "menuitem for " + pref + " is not checked");
});
}
// Set first pref in each category to true
for (let category in prefs) {
hud.setFilterState(prefs[category][0], true);
}
// Re-init the console
closeConsole().then(() => {
openConsole().then(deferred.resolve);
});
return deferred.promise;
}
function onConsoleReopen2(hud) {
info("testing after reopening again");
let hudBox = hud.ui.rootElement;
// Check the main category button is checked and first menuitem is checked
for (let category in prefs) {
let button = hudBox.querySelector(".webconsole-filter-button[category=\""
+ category + "\"]");
ok(isChecked(button), category + " button is checked when first pref is true");
let pref = prefs[category][0];
let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]");
ok(isChecked(menuitem), "first " + category + " menuitem is checked");
}
}
function isChecked(aNode) {
return aNode.getAttribute("checked") === "true";
}
function isUnchecked(aNode) {
return aNode.getAttribute("checked") === "false";
}
@@ -0,0 +1,44 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the basic console.log()-style APIs and filtering work for sharedWorkers
"use strict";
const TEST_URI = "http://example.com/browser/toolkit/devtools/webconsole/test/test-console-workers.html";
function pushPrefEnv()
{
let deferred = promise.defer();
let options = {'set': [["dom.workers.sharedWorkers.enabled", true]]};
SpecialPowers.pushPrefEnv(options, deferred.resolve);
return deferred.promise;
}
let test = asyncTest(function*() {
yield pushPrefEnv();
yield loadTab(TEST_URI);
let hud = yield openConsole();
yield waitForMessages({
webconsole: hud,
messages: [{
text: "foo-bar-shared-worker"
}],
});
hud.setFilterState('sharedworkers', false);
is(hud.outputNode.querySelectorAll(".filtered-by-type").length, 1,
"1 message hidden for sharedworkers (logging turned off)")
hud.setFilterState('sharedworkers', true);
is(hud.outputNode.querySelectorAll(".filtered-by-type").length, 0,
"1 message shown for sharedworkers (logging turned on)")
hud.setFilterState('sharedworkers', false);
hud.jsterm.clearOutput(true);
});
@@ -0,0 +1,214 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* ***** BEGIN LICENSE BLOCK *****
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*
* Contributor(s):
* Julian Viereck <jviereck@mozilla.com>
* Patrick Walton <pcwalton@mozilla.com>
* Mihai Șucan <mihai.sucan@gmail.com>
*
* ***** END LICENSE BLOCK ***** */
// Tests that network log messages bring up the network panel.
const TEST_URI = "data:text/html;charset=utf-8,Web Console network logging tests";
const TEST_NETWORK_REQUEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-network-request.html";
const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png";
const TEST_DATA_JSON_CONTENT =
'{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }';
let lastRequest = null;
let requestCallback = null;
let browser, hud;
function test()
{
loadTab(TEST_URI).then((tab) => {
browser = tab.browser;
openConsole().then((aHud) => {
hud = aHud;
HUDService.lastFinishedRequest.callback = requestCallbackWrapper;
executeSoon(testPageLoad);
});
});
}
function requestCallbackWrapper(aRequest)
{
lastRequest = aRequest;
hud.ui.webConsoleClient.getResponseContent(lastRequest.actor,
function(aResponse) {
lastRequest.response.content = aResponse.content;
lastRequest.discardResponseBody = aResponse.contentDiscarded;
hud.ui.webConsoleClient.getRequestPostData(lastRequest.actor,
function(aResponse) {
lastRequest.request.postData = aResponse.postData;
lastRequest.discardRequestBody = aResponse.postDataDiscarded;
if (requestCallback) {
requestCallback();
}
});
});
}
function testPageLoad()
{
requestCallback = function() {
// Check if page load was logged correctly.
ok(lastRequest, "Page load was logged");
is(lastRequest.request.url, TEST_NETWORK_REQUEST_URI,
"Logged network entry is page load");
is(lastRequest.request.method, "GET", "Method is correct");
ok(!lastRequest.request.postData.text, "No request body was stored");
ok(lastRequest.discardRequestBody, "Request body was discarded");
ok(!lastRequest.response.content.text, "No response body was stored");
ok(lastRequest.discardResponseBody || lastRequest.fromCache,
"Response body was discarded or response came from the cache");
lastRequest = null;
requestCallback = null;
executeSoon(testPageLoadBody);
};
content.location = TEST_NETWORK_REQUEST_URI;
}
function testPageLoadBody()
{
// Turn on logging of request bodies and check again.
hud.ui.setSaveRequestAndResponseBodies(true).then(() => {
ok(hud.ui._saveRequestAndResponseBodies,
"The saveRequestAndResponseBodies property was successfully set.");
testPageLoadBodyAfterSettingUpdate();
});
}
function testPageLoadBodyAfterSettingUpdate()
{
let loaded = false;
let requestCallbackInvoked = false;
requestCallback = function() {
ok(lastRequest, "Page load was logged again");
ok(!lastRequest.discardResponseBody, "Response body was not discarded");
is(lastRequest.response.content.text.indexOf("<!DOCTYPE HTML>"), 0,
"Response body's beginning is okay");
lastRequest = null;
requestCallback = null;
requestCallbackInvoked = true;
if (loaded) {
executeSoon(testXhrGet);
}
};
browser.addEventListener("load", function onLoad() {
browser.removeEventListener("load", onLoad, true);
loaded = true;
if (requestCallbackInvoked) {
executeSoon(testXhrGet);
}
}, true);
content.location.reload();
}
function testXhrGet()
{
requestCallback = function() {
ok(lastRequest, "testXhrGet() was logged");
is(lastRequest.request.method, "GET", "Method is correct");
ok(!lastRequest.request.postData.text, "No request body was sent");
ok(!lastRequest.discardRequestBody, "Request body was not discarded");
is(lastRequest.response.content.text, TEST_DATA_JSON_CONTENT,
"Response is correct");
lastRequest = null;
requestCallback = null;
executeSoon(testXhrPost);
};
// Start the XMLHttpRequest() GET test.
content.wrappedJSObject.testXhrGet();
}
function testXhrPost()
{
requestCallback = function() {
ok(lastRequest, "testXhrPost() was logged");
is(lastRequest.request.method, "POST", "Method is correct");
is(lastRequest.request.postData.text, "Hello world!",
"Request body was logged");
is(lastRequest.response.content.text, TEST_DATA_JSON_CONTENT,
"Response is correct");
lastRequest = null;
requestCallback = null;
executeSoon(testFormSubmission);
};
// Start the XMLHttpRequest() POST test.
content.wrappedJSObject.testXhrPost();
}
function testFormSubmission()
{
// Start the form submission test. As the form is submitted, the page is
// loaded again. Bind to the load event to catch when this is done.
requestCallback = function() {
ok(lastRequest, "testFormSubmission() was logged");
is(lastRequest.request.method, "POST", "Method is correct");
isnot(lastRequest.request.postData.text.
indexOf("Content-Type: application/x-www-form-urlencoded"), -1,
"Content-Type is correct");
isnot(lastRequest.request.postData.text.
indexOf("Content-Length: 20"), -1, "Content-length is correct");
isnot(lastRequest.request.postData.text.
indexOf("name=foo+bar&age=144"), -1, "Form data is correct");
is(lastRequest.response.content.text.indexOf("<!DOCTYPE HTML>"), 0,
"Response body's beginning is okay");
executeSoon(testNetworkPanel);
};
let form = content.document.querySelector("form");
ok(form, "we have the HTML form");
form.submit();
}
function testNetworkPanel()
{
// Open the NetworkPanel. The functionality of the NetworkPanel is tested
// within separate test files.
let networkPanel = hud.ui.openNetworkPanel(hud.ui.filterBox, lastRequest);
networkPanel.panel.addEventListener("popupshown", function onPopupShown() {
networkPanel.panel.removeEventListener("popupshown", onPopupShown, true);
is(hud.ui.filterBox._netPanel, networkPanel,
"Network panel stored on anchor node");
ok(true, "NetworkPanel was opened");
// All tests are done. Shutdown.
networkPanel.panel.hidePopup();
lastRequest = null;
HUDService.lastFinishedRequest.callback = null;
browser = requestCallback = hud = null;
executeSoon(finishTest);
}, true);
}
@@ -0,0 +1,71 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const TEST_URI = "data:text/html;charset=utf8,Test that the web console " +
"displays requests that have been recorded in the " +
"netmonitor, even if the console hadn't opened yet.";
const TEST_FILE = "test-network-request.html";
const TEST_PATH = "http://example.com/browser/toolkit/devtools/webconsole/test/" +
TEST_FILE;
const NET_PREF = "devtools.webconsole.filter.networkinfo";
Services.prefs.setBoolPref(NET_PREF, true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref(NET_PREF);
});
add_task(function* () {
let { tab, browser } = yield loadTab(TEST_URI);
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
info("Network panel is open.");
yield loadDocument(browser);
info("Document loaded.");
// Test that the request appears in the network panel.
testNetmonitor(toolbox);
// Test that the request appears in the console.
let hud = yield openConsole();
info("Web console is open");
yield waitForMessages({
webconsole: hud,
messages: [
{
name: "network message",
text: TEST_FILE,
category: CATEGORY_NETWORK,
severity: SEVERITY_LOG
}
]
});
});
function loadDocument(browser) {
let deferred = promise.defer();
browser.addEventListener("load", function onLoad() {
browser.removeEventListener("load", onLoad, true);
deferred.resolve();
}, true);
content.location = TEST_PATH;
return deferred.promise;
}
function testNetmonitor(toolbox) {
let monitor = toolbox.getCurrentPanel();
let { RequestsMenu } = monitor.panelWin.NetMonitorView;
is(RequestsMenu.itemCount, 1, "Network request appears in the network panel");
let item = RequestsMenu.getItemAtIndex(0);
is(item.attachment.method, "GET", "The attached method is correct.");
is(item.attachment.url, TEST_PATH, "The attached url is correct.");
}
@@ -10,6 +10,7 @@ support-files =
[test_basics.html]
[test_bug819670_getter_throws.html]
[test_cached_messages.html]
[test_commands_registration.html]
[test_consoleapi.html]
[test_consoleapi_innerID.html]
[test_file_uri.html]
@@ -0,0 +1,13 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE HTML>
<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
<meta charset="utf-8">
<title>Console test</title>
</head>
<body>
<script type="text/javascript">
var sw = new SharedWorker('data:application/javascript,console.log("foo-bar-shared-worker");');
</script>
</body>
</html>
@@ -0,0 +1,192 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for Web Console commands registration.</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript;version=1.8" src="common.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for Web Console commands registration.</p>
<p id="quack"></p>
<script class="testbody" type="text/javascript;version=1.8">
SimpleTest.waitForExplicitFinish();
let gState;
let tests;
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let {WebConsoleCommands} = devtools.require("devtools/toolkit/webconsole/utils");
function evaluateJS(input) {
return new Promise((resolve) => gState.client.evaluateJS(input, resolve));
}
function* evaluateJSAndCheckResult(input, result) {
let response = yield evaluateJS(input);
checkObject(response, {result});
}
function startTest()
{
removeEventListener("load", startTest);
attachConsole(["PageError"], onAttach, true);
}
function onAttach(aState, aResponse)
{
gState = aState;
runTests(tests, testEnd);
}
tests = [
Task.async(function* registerNewCommand() {
let win;
WebConsoleCommands.register("setFoo", (owner, value) => {
owner.window.foo = value;
return "ok";
});
ok(WebConsoleCommands.hasCommand("setFoo"),
"The command should be registered");
let command = "setFoo('bar')";
let response = yield evaluateJS(command);
checkObject(response, {
from: gState.actor,
input: command,
result: "ok"
});
is(top.foo, "bar", "top.foo should equal to 'bar'");
nextTest();
}),
Task.async(function* wrapCommand() {
let origKeys = WebConsoleCommands.getCommand("keys");
let newKeys = (...args) => {
let [owner, arg0] = args;
if (arg0 === ">o_/") {
return "bang!";
}
else {
return origKeys(...args);
}
};
WebConsoleCommands.register("keys", newKeys);
is(WebConsoleCommands.getCommand("keys"), newKeys,
"the keys() command should have been replaced");
let response = yield evaluateJS("keys('>o_/')");
checkObject(response, {
from: gState.actor,
result: "bang!"
});
response = yield evaluateJS("keys({foo: 'bar'})");
checkObject(response, {
from: gState.actor,
result: {
class: "Array",
preview: {
items: ["foo"]
}
}
});
WebConsoleCommands.register("keys", origKeys);
is(WebConsoleCommands.getCommand("keys"), origKeys,
"the keys() command should be restored");
nextTest();
}),
Task.async(function* unregisterCommand() {
WebConsoleCommands.unregister("setFoo");
let response = yield evaluateJS("setFoo");
checkObject(response, {
from: gState.actor,
input: "setFoo",
result: {
type: "undefined"
},
exceptionMessage: /setFoo is not defined/
});
nextTest();
}),
Task.async(function* registerAccessor() {
WebConsoleCommands.register("$foo", {
get(owner) {
let foo = owner.window.frames[0].window.document.getElementById("quack");
return owner.makeDebuggeeValue(foo);
}
});
let command = "$foo.textContent = '>o_/'";
let response = yield evaluateJS(command);
checkObject(response, {
from: gState.actor,
input: command,
result: ">o_/"
});
is(document.getElementById("quack").textContent, ">o_/",
"#foo textContent should equal to \">o_/\"");
WebConsoleCommands.unregister("$foo");
ok(!WebConsoleCommands.hasCommand("$foo"), "$foo should be unregistered");
nextTest();
}),
Task.async(function* unregisterAfterOverridingTwice() {
WebConsoleCommands.register("keys", (owner, obj) => "command 1");
info("checking the value of the first override");
yield evaluateJSAndCheckResult("keys('foo');", "command 1");
let orig = WebConsoleCommands.getCommand("keys");
WebConsoleCommands.register("keys", (owner, obj) => {
if (obj === "quack")
return "bang!";
return orig(owner, obj);
});
info("checking the values after the second override");
yield evaluateJSAndCheckResult("keys({});", "command 1");
yield evaluateJSAndCheckResult("keys('quack');", "bang!");
WebConsoleCommands.unregister("keys");
info("checking the value after unregistration (should restore " +
"the original command)");
yield evaluateJSAndCheckResult("keys({});", {
class: "Array",
preview: {items: []}
});
nextTest();
})
];
function testEnd()
{
// If this is the first run, reload the page and do it again.
// Otherwise, end the test.
delete top.foo;
closeDebugger(gState, function() {
gState = null;
SimpleTest.finish();
});
}
addEventListener("load", startTest);
</script>
</body>
</html>
+403 -269
View File
@@ -14,10 +14,10 @@ loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
loader.lazyImporter(this, "LayoutHelpers", "resource://gre/modules/devtools/LayoutHelpers.jsm");
// TODO: Bug 842672 - toolkit/ imports modules from browser/.
// Note that these are only used in JSTermHelpers, see $0 and pprint().
loader.lazyImporter(this, "gDevTools", "resource://gre/modules/devtools/gDevTools.jsm");
// Note that these are only used in WebConsoleCommands, see $0 and pprint().
loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm");
loader.lazyImporter(this, "VariablesView", "resource://gre/modules/devtools/VariablesView.jsm");
loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
loader.lazyImporter(this, "DevToolsUtils", "resource://gre/modules/devtools/DevToolsUtils.jsm");
// Match the function name from the result of toString() or toSource().
@@ -32,13 +32,15 @@ const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;
const REGEX_MATCH_FUNCTION_ARGS = /^\(?function\s*[^\s(]*\s*\((.+?)\)/;
// Number of terminal entries for the self-xss prevention to go away
const CONSOLE_ENTRY_THRESHOLD = 5
const CONSOLE_ENTRY_THRESHOLD = 5;
// Provide an easy way to bail out of even attempting an autocompletion
// if an object has way too many properties. Protects against large objects
// with numeric values that wouldn't be tallied towards MAX_AUTOCOMPLETIONS.
const MAX_AUTOCOMPLETE_ATTEMPTS = exports.MAX_AUTOCOMPLETE_ATTEMPTS = 100000;
const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [ 'SharedWorker', 'ServiceWorker', 'Worker' ];
// Prevent iterating over too many properties during autocomplete suggestions.
const MAX_AUTOCOMPLETIONS = exports.MAX_AUTOCOMPLETIONS = 1500;
@@ -555,7 +557,7 @@ let WebConsoleUtils = {
_usageCount: 0,
get usageCount() {
if (WebConsoleUtils._usageCount < CONSOLE_ENTRY_THRESHOLD) {
WebConsoleUtils._usageCount = Services.prefs.getIntPref("devtools.selfxss.count")
WebConsoleUtils._usageCount = Services.prefs.getIntPref("devtools.selfxss.count");
if (Services.prefs.getBoolPref("devtools.chrome.enabled")) {
WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD;
}
@@ -917,7 +919,7 @@ function JSPropertyProvider(aDbgObject, anEnvironment, aInputValue, aCursor)
return null;
}
if (/\[\d+\]$/.test(prop)) {
if (/\[\d+\]$/.test(prop)) {
// The property to autocomplete is a member of array. For example
// list[i][j]..[n]. Traverse the array to get the actual element.
obj = getArrayMemberProperty(obj, prop);
@@ -1449,7 +1451,7 @@ ConsoleAPIListener.prototype =
}
let apiMessage = aMessage.wrappedJSObject;
if (this.window) {
if (this.window && CONSOLE_WORKER_IDS.indexOf(apiMessage.innerID) == -1) {
let msgWindow = Services.wm.getCurrentInnerWindowWithId(apiMessage.innerID);
if (!msgWindow || !this.layoutHelpers.isIncludedInTopLevelWindow(msgWindow)) {
// Not the same window!
@@ -1489,6 +1491,10 @@ ConsoleAPIListener.prototype =
});
}
CONSOLE_WORKER_IDS.forEach((id) => {
messages = messages.concat(ConsoleAPIStorage.getEvents(id));
});
if (this.consoleID) {
messages = messages.filter((m) => m.consoleID == this.consoleID);
}
@@ -1510,297 +1516,425 @@ ConsoleAPIListener.prototype =
},
};
/**
* WebConsole commands manager.
*
* Defines a set of functions /variables ("commands") that are available from
* the Web Console but not from the web page.
*
*/
let WebConsoleCommands = {
_registeredCommands: new Map(),
_originalCommands: new Map(),
/**
* @private
* Reserved for built-in commands. To register a command from the code of an
* add-on, see WebConsoleCommands.register instead.
*
* @see WebConsoleCommands.register
*/
_registerOriginal: function (name, command) {
this.register(name, command);
this._originalCommands.set(name, this.getCommand(name));
},
/**
* Register a new command.
* @param {string} name The command name (exemple: "$")
* @param {(function|object)} command The command to register.
* It can be a function so the command is a function (like "$()"),
* or it can also be a property descriptor to describe a getter / value (like
* "$0").
*
* The command function or the command getter are passed a owner object as
* their first parameter (see the example below).
*
* Note that setters don't work currently and "enumerable" and "configurable"
* are forced to true.
*
* @example
*
* WebConsoleCommands.register("$", function JSTH_$(aOwner, aSelector)
* {
* return aOwner.window.document.querySelector(aSelector);
* });
*
* WebConsoleCommands.register("$0", {
* get: function(aOwner) {
* return aOwner.makeDebuggeeValue(aOwner.selectedNode);
* }
* });
*/
register: function(name, command) {
this._registeredCommands.set(name, command);
},
/**
* Unregister a command.
*
* If the command being unregister overrode a built-in command,
* the latter is restored.
*
* @param {string} name The name of the command
*/
unregister: function(name) {
this._registeredCommands.delete(name);
if (this._originalCommands.has(name)) {
this.register(name, this._originalCommands.get(name));
}
},
/**
* Returns a command by its name.
*
* @param {string} name The name of the command.
*
* @return {(function|object)} The command.
*/
getCommand: function(name) {
return this._registeredCommands.get(name);
},
/**
* Returns true if a command is registered with the given name.
*
* @param {string} name The name of the command.
*
* @return {boolean} True if the command is registered.
*/
hasCommand: function(name) {
return this._registeredCommands.has(name);
},
};
exports.WebConsoleCommands = WebConsoleCommands;
/*
* Built-in commands.
*
* A list of helper functions used by Firebug can be found here:
* http://getfirebug.com/wiki/index.php/Command_Line_API
*/
/**
* Find a node by ID.
*
* @param string aId
* The ID of the element you want.
* @return nsIDOMNode or null
* The result of calling document.querySelector(aSelector).
*/
WebConsoleCommands._registerOriginal("$", function JSTH_$(aOwner, aSelector)
{
return aOwner.window.document.querySelector(aSelector);
});
/**
* Find the nodes matching a CSS selector.
*
* @param string aSelector
* A string that is passed to window.document.querySelectorAll.
* @return nsIDOMNodeList
* Returns the result of document.querySelectorAll(aSelector).
*/
WebConsoleCommands._registerOriginal("$$", function JSTH_$$(aOwner, aSelector)
{
return aOwner.window.document.querySelectorAll(aSelector);
});
/**
* Returns the result of the last console input evaluation
*
* @return object|undefined
* Returns last console evaluation or undefined
*/
WebConsoleCommands._registerOriginal("$_", {
get: function(aOwner) {
return aOwner.consoleActor.getLastConsoleInputEvaluation();
}
});
/**
* JSTerm helper functions.
* Runs an xPath query and returns all matched nodes.
*
* Defines a set of functions ("helper functions") that are available from the
* Web Console but not from the web page.
*
* A list of helper functions used by Firebug can be found here:
* http://getfirebug.com/wiki/index.php/Command_Line_API
*
* @param object aOwner
* The owning object.
* @param string aXPath
* xPath search query to execute.
* @param [optional] nsIDOMNode aContext
* Context to run the xPath query on. Uses window.document if not set.
* @return array of nsIDOMNode
*/
function JSTermHelpers(aOwner)
WebConsoleCommands._registerOriginal("$x", function JSTH_$x(aOwner, aXPath, aContext)
{
/**
* Find a node by ID.
*
* @param string aId
* The ID of the element you want.
* @return nsIDOMNode or null
* The result of calling document.querySelector(aSelector).
*/
aOwner.sandbox.$ = function JSTH_$(aSelector)
{
return aOwner.window.document.querySelector(aSelector);
let nodes = new aOwner.window.wrappedJSObject.Array();
let doc = aOwner.window.document;
aContext = aContext || doc;
let results = doc.evaluate(aXPath, aContext, null,
Ci.nsIDOMXPathResult.ANY_TYPE, null);
let node;
while ((node = results.iterateNext())) {
nodes.push(node);
}
return nodes;
});
/**
* Returns the currently selected object in the highlighter.
*
* @return Object representing the current selection in the
* Inspector, or null if no selection exists.
*/
WebConsoleCommands._registerOriginal("$0", {
get: function(aOwner) {
return aOwner.makeDebuggeeValue(aOwner.selectedNode);
}
});
/**
* Clears the output of the WebConsole.
*/
WebConsoleCommands._registerOriginal("clear", function JSTH_clear(aOwner)
{
aOwner.helperResult = {
type: "clearOutput",
};
});
/**
* Find the nodes matching a CSS selector.
*
* @param string aSelector
* A string that is passed to window.document.querySelectorAll.
* @return nsIDOMNodeList
* Returns the result of document.querySelectorAll(aSelector).
*/
aOwner.sandbox.$$ = function JSTH_$$(aSelector)
{
return aOwner.window.document.querySelectorAll(aSelector);
/**
* Clears the input history of the WebConsole.
*/
WebConsoleCommands._registerOriginal("clearHistory", function JSTH_clearHistory(aOwner)
{
aOwner.helperResult = {
type: "clearHistory",
};
});
/**
* Returns the result of the last console input evaluation
*
* @return object|undefined
* Returns last console evaluation or undefined
*/
Object.defineProperty(aOwner.sandbox, "$_", {
get: function() {
return aOwner.consoleActor.getLastConsoleInputEvaluation();
},
enumerable: true,
configurable: true
});
/**
* Returns the result of Object.keys(aObject).
*
* @param object aObject
* Object to return the property names from.
* @return array of strings
*/
WebConsoleCommands._registerOriginal("keys", function JSTH_keys(aOwner, aObject)
{
return aOwner.window.wrappedJSObject.Object.keys(WebConsoleUtils.unwrap(aObject));
});
/**
* Runs an xPath query and returns all matched nodes.
*
* @param string aXPath
* xPath search query to execute.
* @param [optional] nsIDOMNode aContext
* Context to run the xPath query on. Uses window.document if not set.
* @return array of nsIDOMNode
*/
aOwner.sandbox.$x = function JSTH_$x(aXPath, aContext)
{
let nodes = new aOwner.window.wrappedJSObject.Array();
let doc = aOwner.window.document;
aContext = aContext || doc;
/**
* Returns the values of all properties on aObject.
*
* @param object aObject
* Object to display the values from.
* @return array of string
*/
WebConsoleCommands._registerOriginal("values", function JSTH_values(aOwner, aObject)
{
let arrValues = new aOwner.window.wrappedJSObject.Array();
let obj = WebConsoleUtils.unwrap(aObject);
let results = doc.evaluate(aXPath, aContext, null,
Ci.nsIDOMXPathResult.ANY_TYPE, null);
let node;
while ((node = results.iterateNext())) {
nodes.push(node);
}
for (let prop in obj) {
arrValues.push(obj[prop]);
}
return nodes;
};
return arrValues;
});
/**
* Returns the currently selected object in the highlighter.
*
* @return Object representing the current selection in the
* Inspector, or null if no selection exists.
*/
Object.defineProperty(aOwner.sandbox, "$0", {
get: function() {
return aOwner.makeDebuggeeValue(aOwner.selectedNode)
},
enumerable: true,
configurable: true
});
/**
* Opens a help window in MDN.
*/
WebConsoleCommands._registerOriginal("help", function JSTH_help(aOwner)
{
aOwner.helperResult = { type: "help" };
});
/**
* Clears the output of the JSTerm.
*/
aOwner.sandbox.clear = function JSTH_clear()
{
aOwner.helperResult = {
type: "clearOutput",
};
};
/**
* Returns the result of Object.keys(aObject).
*
* @param object aObject
* Object to return the property names from.
* @return array of strings
*/
aOwner.sandbox.keys = function JSTH_keys(aObject)
{
return aOwner.window.wrappedJSObject.Object.keys(WebConsoleUtils.unwrap(aObject));
};
/**
* Returns the values of all properties on aObject.
*
* @param object aObject
* Object to display the values from.
* @return array of string
*/
aOwner.sandbox.values = function JSTH_values(aObject)
{
let arrValues = new aOwner.window.wrappedJSObject.Array();
let obj = WebConsoleUtils.unwrap(aObject);
for (let prop in obj) {
arrValues.push(obj[prop]);
}
return arrValues;
};
/**
* Opens a help window in MDN.
*/
aOwner.sandbox.help = function JSTH_help()
{
aOwner.helperResult = { type: "help" };
};
/**
* Change the JS evaluation scope.
*
* @param DOMElement|string|window aWindow
* The window object to use for eval scope. This can be a string that
* is used to perform document.querySelector(), to find the iframe that
* you want to cd() to. A DOMElement can be given as well, the
* .contentWindow property is used. Lastly, you can directly pass
* a window object. If you call cd() with no arguments, the current
* eval scope is cleared back to its default (the top window).
*/
aOwner.sandbox.cd = function JSTH_cd(aWindow)
{
if (!aWindow) {
aOwner.consoleActor.evalWindow = null;
aOwner.helperResult = { type: "cd" };
return;
}
if (typeof aWindow == "string") {
aWindow = aOwner.window.document.querySelector(aWindow);
}
if (aWindow instanceof Ci.nsIDOMElement && aWindow.contentWindow) {
aWindow = aWindow.contentWindow;
}
if (!(aWindow instanceof Ci.nsIDOMWindow)) {
aOwner.helperResult = { type: "error", message: "cdFunctionInvalidArgument" };
return;
}
aOwner.consoleActor.evalWindow = aWindow;
/**
* Change the JS evaluation scope.
*
* @param DOMElement|string|window aWindow
* The window object to use for eval scope. This can be a string that
* is used to perform document.querySelector(), to find the iframe that
* you want to cd() to. A DOMElement can be given as well, the
* .contentWindow property is used. Lastly, you can directly pass
* a window object. If you call cd() with no arguments, the current
* eval scope is cleared back to its default (the top window).
*/
WebConsoleCommands._registerOriginal("cd", function JSTH_cd(aOwner, aWindow)
{
if (!aWindow) {
aOwner.consoleActor.evalWindow = null;
aOwner.helperResult = { type: "cd" };
};
return;
}
/**
* Inspects the passed aObject. This is done by opening the PropertyPanel.
*
* @param object aObject
* Object to inspect.
*/
aOwner.sandbox.inspect = function JSTH_inspect(aObject)
{
let dbgObj = aOwner.makeDebuggeeValue(aObject);
let grip = aOwner.createValueGrip(dbgObj);
if (typeof aWindow == "string") {
aWindow = aOwner.window.document.querySelector(aWindow);
}
if (aWindow instanceof Ci.nsIDOMElement && aWindow.contentWindow) {
aWindow = aWindow.contentWindow;
}
if (!(aWindow instanceof Ci.nsIDOMWindow)) {
aOwner.helperResult = { type: "error", message: "cdFunctionInvalidArgument" };
return;
}
aOwner.consoleActor.evalWindow = aWindow;
aOwner.helperResult = { type: "cd" };
});
/**
* Inspects the passed aObject. This is done by opening the PropertyPanel.
*
* @param object aObject
* Object to inspect.
*/
WebConsoleCommands._registerOriginal("inspect", function JSTH_inspect(aOwner, aObject)
{
let dbgObj = aOwner.makeDebuggeeValue(aObject);
let grip = aOwner.createValueGrip(dbgObj);
aOwner.helperResult = {
type: "inspectObject",
input: aOwner.evalInput,
object: grip,
};
});
/**
* Prints aObject to the output.
*
* @param object aObject
* Object to print to the output.
* @return string
*/
WebConsoleCommands._registerOriginal("pprint", function JSTH_pprint(aOwner, aObject)
{
if (aObject === null || aObject === undefined || aObject === true ||
aObject === false) {
aOwner.helperResult = {
type: "inspectObject",
input: aOwner.evalInput,
object: grip,
type: "error",
message: "helperFuncUnsupportedTypeError",
};
return null;
}
aOwner.helperResult = { rawOutput: true };
if (typeof aObject == "function") {
return aObject + "\n";
}
let output = [];
let obj = WebConsoleUtils.unwrap(aObject);
for (let name in obj) {
let desc = WebConsoleUtils.getPropertyDescriptor(obj, name) || {};
if (desc.get || desc.set) {
// TODO: Bug 842672 - toolkit/ imports modules from browser/.
let getGrip = VariablesView.getGrip(desc.get);
let setGrip = VariablesView.getGrip(desc.set);
let getString = VariablesView.getString(getGrip);
let setString = VariablesView.getString(setGrip);
output.push(name + ":", " get: " + getString, " set: " + setString);
}
else {
let valueGrip = VariablesView.getGrip(obj[name]);
let valueString = VariablesView.getString(valueGrip);
output.push(name + ": " + valueString);
}
}
return " " + output.join("\n ");
});
/**
* Print the String representation of a value to the output, as-is.
*
* @param any aValue
* A value you want to output as a string.
* @return void
*/
WebConsoleCommands._registerOriginal("print", function JSTH_print(aOwner, aValue)
{
aOwner.helperResult = { rawOutput: true };
if (typeof aValue === "symbol") {
return Symbol.prototype.toString.call(aValue);
}
// Waiving Xrays here allows us to see a closer representation of the
// underlying object. This may execute arbitrary content code, but that
// code will run with content privileges, and the result will be rendered
// inert by coercing it to a String.
return String(Cu.waiveXrays(aValue));
});
/**
* Copy the String representation of a value to the clipboard.
*
* @param any aValue
* A value you want to copy as a string.
* @return void
*/
WebConsoleCommands._registerOriginal("copy", function JSTH_copy(aOwner, aValue)
{
let payload;
try {
if (aValue instanceof Ci.nsIDOMElement) {
payload = aValue.outerHTML;
} else if (typeof aValue == "string") {
payload = aValue;
} else {
payload = JSON.stringify(aValue, null, " ");
}
} catch (ex) {
payload = "/* " + ex + " */";
}
aOwner.helperResult = {
type: "copyValueToClipboard",
value: payload,
};
});
/**
* Prints aObject to the output.
*
* @param object aObject
* Object to print to the output.
* @return string
*/
aOwner.sandbox.pprint = function JSTH_pprint(aObject)
{
if (aObject === null || aObject === undefined || aObject === true ||
aObject === false) {
aOwner.helperResult = {
type: "error",
message: "helperFuncUnsupportedTypeError",
};
return null;
/**
* (Internal only) Add the bindings to |owner.sandbox|.
* This is intended to be used by the WebConsole actor only.
*
* @param object aOwner
* The owning object.
*/
function addWebConsoleCommands(owner) {
if (!owner) {
throw new Error("The owner is required");
}
for (let [name, command] of WebConsoleCommands._registeredCommands) {
if (typeof command === "function") {
owner.sandbox[name] = command.bind(undefined, owner);
}
else if (typeof command === "object") {
let clone = Object.assign({}, command, {
// We force the enumerability and the configurability (so the
// WebConsoleActor can reconfigure the property).
enumerable: true,
configurable: true
});
aOwner.helperResult = { rawOutput: true };
if (typeof aObject == "function") {
return aObject + "\n";
}
let output = [];
let obj = WebConsoleUtils.unwrap(aObject);
for (let name in obj) {
let desc = WebConsoleUtils.getPropertyDescriptor(obj, name) || {};
if (desc.get || desc.set) {
// TODO: Bug 842672 - toolkit/ imports modules from browser/.
let getGrip = VariablesView.getGrip(desc.get);
let setGrip = VariablesView.getGrip(desc.set);
let getString = VariablesView.getString(getGrip);
let setString = VariablesView.getString(setGrip);
output.push(name + ":", " get: " + getString, " set: " + setString);
if (typeof command.get === "function") {
clone.get = command.get.bind(undefined, owner);
}
else {
let valueGrip = VariablesView.getGrip(obj[name]);
let valueString = VariablesView.getString(valueGrip);
output.push(name + ": " + valueString);
if (typeof command.set === "function") {
clone.set = command.set.bind(undefined, owner);
}
}
return " " + output.join("\n ");
};
/**
* Print the String representation of a value to the output, as-is.
*
* @param any aValue
* A value you want to output as a string.
* @return void
*/
aOwner.sandbox.print = function JSTH_print(aValue)
{
aOwner.helperResult = { rawOutput: true };
if (typeof aValue === "symbol") {
return Symbol.prototype.toString.call(aValue);
Object.defineProperty(owner.sandbox, name, clone);
}
// Waiving Xrays here allows us to see a closer representation of the
// underlying object. This may execute arbitrary content code, but that
// code will run with content privileges, and the result will be rendered
// inert by coercing it to a String.
return String(Cu.waiveXrays(aValue));
};
/**
* Copy the String representation of a value to the clipboard.
*
* @param any aValue
* A value you want to copy as a string.
* @return void
*/
aOwner.sandbox.copy = function JSTH_copy(aValue)
{
let payload;
try {
if (aValue instanceof Ci.nsIDOMElement) {
payload = aValue.outerHTML;
} else if (typeof aValue == "string") {
payload = aValue;
} else {
payload = JSON.stringify(aValue, null, " ");
}
} catch (ex) {
payload = "/* " + ex + " */";
}
aOwner.helperResult = {
type: "copyValueToClipboard",
value: payload,
};
};
}
}
exports.JSTermHelpers = JSTermHelpers;
exports.addWebConsoleCommands = addWebConsoleCommands;
/**
* A ReflowObserver that listens for reflow events from the page.
+149 -123
View File
@@ -8,7 +8,7 @@
const {Cc, Ci, Cu} = require("chrome");
let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
const {Utils: WebConsoleUtils, CONSOLE_WORKER_IDS} = require("devtools/toolkit/webconsole/utils");
loader.lazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
@@ -26,6 +26,8 @@ loader.lazyGetter(this, "ConsoleOutput",
() => require("devtools/webconsole/console-output").ConsoleOutput);
loader.lazyGetter(this, "Messages",
() => require("devtools/webconsole/console-output").Messages);
loader.lazyGetter(this, "asyncStorage",
() => require("devtools/toolkit/shared/async-storage"));
loader.lazyImporter(this, "EnvironmentClient", "resource://gre/modules/devtools/dbg-client.jsm");
loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm");
loader.lazyImporter(this, "VariablesView", "resource://gre/modules/devtools/VariablesView.jsm");
@@ -136,6 +138,10 @@ const LEVELS = {
count: SEVERITY_LOG
};
// This array contains the prefKey for the workers and it must keep them in the
// same order as CONSOLE_WORKER_IDS
const WORKERTYPES_PREFKEYS = [ 'sharedworkers', 'serviceworkers', 'windowlessworkers' ];
// The lowest HTTP response code (inclusive) that is considered an error.
const MIN_HTTP_ERROR_CODE = 400;
// The highest HTTP response code (inclusive) that is considered an error.
@@ -176,6 +182,7 @@ const MIN_FONT_SIZE = 10;
const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout";
const PREF_PERSISTLOG = "devtools.webconsole.persistlog";
const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
const PREF_INPUT_HISTORY_COUNT = "devtools.webconsole.inputHistoryCount";
/**
* A WebConsoleFrame instance is an interactive console initialized *per target*
@@ -199,7 +206,6 @@ function WebConsoleFrame(aWebConsoleOwner)
this._outputQueue = [];
this._itemDestroyQueue = [];
this._pruneCategoriesQueue = {};
this._networkRequests = {};
this.filterPrefs = {};
this.output = new ConsoleOutput(this);
@@ -247,14 +253,6 @@ WebConsoleFrame.prototype = {
*/
_initDefer: null,
/**
* Holds the network requests currently displayed by the Web Console. Each key
* represents the connection ID and the value is network request information.
* @private
* @type object
*/
_networkRequests: null,
/**
* Last time when we displayed any message in the output.
*
@@ -447,12 +445,29 @@ WebConsoleFrame.prototype = {
/**
* Initialize the WebConsoleFrame instance.
* @return object
* A promise object for the initialization.
* A promise object that resolves once the frame is ready to use.
*/
init: function WCF_init()
init: function()
{
this._initUI();
return this._initConnection();
let connectionInited = this._initConnection();
// Don't reject if the history fails to load for some reason.
// This would be fine, the panel will just start with empty history.
let allReady = this.jsterm.historyLoaded.catch(() => {}).then(() => {
return connectionInited;
});
// This notification is only used in tests. Don't chain it onto
// the returned promise because the console panel needs to be attached
// to the toolbox before the web-console-created event is receieved.
let notifyObservers = () => {
let id = WebConsoleUtils.supportsString(this.hudId);
Services.obs.notifyObservers(id, "web-console-created", null);
};
allReady.then(notifyObservers, notifyObservers);
return allReady;
},
/**
@@ -479,9 +494,6 @@ WebConsoleFrame.prototype = {
aReason.error + ": " + aReason.message);
this.outputMessage(CATEGORY_JS, node, [aReason]);
this._initDefer.reject(aReason);
}).then(() => {
let id = WebConsoleUtils.supportsString(this.hudId);
Services.obs.notifyObservers(id, "web-console-created", null);
});
return this._initDefer.promise;
@@ -632,7 +644,8 @@ WebConsoleFrame.prototype = {
{
let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
"exception", "jswarn", "jslog", "error", "info", "warn", "log",
"secerror", "secwarn", "netwarn", "netxhr"];
"secerror", "secwarn", "netwarn", "netxhr", "sharedworkers",
"serviceworkers", "windowlessworkers"];
for (let pref of prefs) {
this.filterPrefs[pref] = Services.prefs
.getBoolPref(this._filterPrefsPrefix + pref);
@@ -1009,8 +1022,11 @@ WebConsoleFrame.prototype = {
// (filter="error", filter="cssparser", etc.) and add or remove the
// "filtered-by-type" class, which turns on or off the display.
let attribute = WORKERTYPES_PREFKEYS.indexOf(aPrefKey) == -1
? 'filter' : 'workerType';
let xpath = ".//*[contains(@class, 'message') and " +
"@filter='" + aPrefKey + "']";
"@" + attribute + "='" + aPrefKey + "']";
let result = doc.evaluate(xpath, outputNode, null,
Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0; i < result.snapshotLength; i++) {
@@ -1071,6 +1087,12 @@ WebConsoleFrame.prototype = {
isFiltered = true;
}
// Filter by worker type
if ("workerType" in aNode && !this.getFilterState(aNode.workerType)) {
aNode.classList.add("filtered-by-type");
isFiltered = true;
}
// Filter on the search string.
let search = this.filterBox.value;
let text = aNode.clipboardText;
@@ -1192,6 +1214,9 @@ WebConsoleFrame.prototype = {
this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
[aMessage]);
break;
case "NetworkEvent":
this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aMessage]);
break;
}
}, this);
},
@@ -1348,6 +1373,12 @@ WebConsoleFrame.prototype = {
}
}
let workerTypeID = CONSOLE_WORKER_IDS.indexOf(aMessage.workerType);
if (workerTypeID != -1) {
node.workerType = WORKERTYPES_PREFKEYS[workerTypeID];
node.setAttribute('workerType', WORKERTYPES_PREFKEYS[workerTypeID]);
}
return node;
},
@@ -1486,19 +1517,14 @@ WebConsoleFrame.prototype = {
/**
* Log network event.
*
* @param object aActor
* The network event actor to log.
* @param object networkInfo
* The network request information to log.
* @return nsIDOMElement|null
* The message element to display in the Web Console output.
*/
logNetEvent: function WCF_logNetEvent(aActor)
logNetEvent: function(networkInfo)
{
let actorId = aActor.actor;
let networkInfo = this._networkRequests[actorId];
if (!networkInfo) {
return null;
}
let actorId = networkInfo.actor;
let request = networkInfo.request;
let clipboardText = request.method + " " + request.url;
let severity = SEVERITY_LOG;
@@ -1518,7 +1544,8 @@ WebConsoleFrame.prototype = {
let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity,
methodNode, null, null,
clipboardText);
clipboardText, null,
networkInfo.timeStamp);
if (networkInfo.private) {
messageNode.setAttribute("private", true);
}
@@ -1756,84 +1783,29 @@ WebConsoleFrame.prototype = {
/**
* Handle the network events coming from the remote Web Console.
*
* @param object aActor
* The NetworkEventActor grip.
* @param object networkInfo
* The network request information.
*/
handleNetworkEvent: function WCF_handleNetworkEvent(aActor)
handleNetworkEvent: function(networkInfo)
{
let networkInfo = {
node: null,
actor: aActor.actor,
discardRequestBody: true,
discardResponseBody: true,
startedDateTime: aActor.startedDateTime,
request: {
url: aActor.url,
method: aActor.method,
},
isXHR: aActor.isXHR,
response: {},
timings: {},
updates: [], // track the list of network event updates
private: aActor.private,
};
this._networkRequests[aActor.actor] = networkInfo;
this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor]);
this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [networkInfo]);
},
/**
* Handle network event updates coming from the server.
*
* @param string aActorId
* The network event actor ID.
* @param string aType
* Update type.
* @param object aPacket
* @param object networkInfo
* The network request information.
* @param object packet
* Update details.
*/
handleNetworkEventUpdate:
function WCF_handleNetworkEventUpdate(aActorId, aType, aPacket)
handleNetworkEventUpdate: function(networkInfo, packet)
{
let networkInfo = this._networkRequests[aActorId];
if (!networkInfo) {
return;
}
networkInfo.updates.push(aType);
switch (aType) {
case "requestHeaders":
networkInfo.request.headersSize = aPacket.headersSize;
break;
case "requestPostData":
networkInfo.discardRequestBody = aPacket.discardRequestBody;
networkInfo.request.bodySize = aPacket.dataSize;
break;
case "responseStart":
networkInfo.response.httpVersion = aPacket.response.httpVersion;
networkInfo.response.status = aPacket.response.status;
networkInfo.response.statusText = aPacket.response.statusText;
networkInfo.response.headersSize = aPacket.response.headersSize;
networkInfo.discardResponseBody = aPacket.response.discardResponseBody;
break;
case "responseContent":
networkInfo.response.content = {
mimeType: aPacket.mimeType,
};
networkInfo.response.bodySize = aPacket.contentSize;
networkInfo.discardResponseBody = aPacket.discardResponseBody;
break;
case "eventTimings":
networkInfo.totalTime = aPacket.totalTime;
break;
}
if (networkInfo.node && this._updateNetMessage(aActorId)) {
if (networkInfo.node && this._updateNetMessage(packet.from)) {
this.emit("new-messages", new Set([{
update: true,
node: networkInfo.node,
response: aPacket,
response: packet,
}]));
}
@@ -1858,7 +1830,7 @@ WebConsoleFrame.prototype = {
*/
_updateNetMessage: function WCF__updateNetMessage(aActorId)
{
let networkInfo = this._networkRequests[aActorId];
let networkInfo = this.webConsoleClient.getNetworkRequest(aActorId);
if (!networkInfo || !networkInfo.node) {
return;
}
@@ -2404,8 +2376,8 @@ WebConsoleFrame.prototype = {
else if (typeof methodOrNode != "function") {
connectionId = methodOrNode._connectionId;
}
if (connectionId && connectionId in this._networkRequests) {
delete this._networkRequests[connectionId];
if (connectionId && this.webConsoleClient.hasNetworkRequest(connectionId)) {
this.webConsoleClient.removeNetworkRequest(connectionId);
this._releaseObject(connectionId);
}
}
@@ -2482,7 +2454,7 @@ WebConsoleFrame.prototype = {
}
else if (aNode._connectionId &&
aNode.category == CATEGORY_NETWORK) {
delete this._networkRequests[aNode._connectionId];
this.webConsoleClient.removeNetworkRequest(aNode._connectionId);
this._releaseObject(aNode._connectionId);
}
else if (aNode.classList.contains("inlined-variables-view")) {
@@ -2979,7 +2951,7 @@ WebConsoleFrame.prototype = {
this._itemDestroyQueue.forEach(this._destroyItem, this);
this._itemDestroyQueue = [];
this._pruneCategoriesQueue = {};
this._networkRequests = {};
this.webConsoleClient.clearNetworkRequests();
if (this._outputTimerInitialized) {
this._outputTimerInitialized = false;
@@ -3058,17 +3030,11 @@ function JSTerm(aWebConsoleFrame)
{
this.hud = aWebConsoleFrame;
this.hudId = this.hud.hudId;
this.inputHistoryCount = Services.prefs.getIntPref(PREF_INPUT_HISTORY_COUNT);
this.lastCompletion = { value: null };
this.history = [];
this._loadHistory();
// Holds the number of entries in history. This value is incremented in
// this.execute().
this.historyIndex = 0; // incremented on this.execute()
// Holds the index of the history entry that the user is currently viewing.
// This is reset to this.history.length when this.execute() is invoked.
this.historyPlaceHolder = 0;
this._objectActorsInVariablesViews = new Map();
this._keyPress = this._keyPress.bind(this);
@@ -3083,6 +3049,53 @@ function JSTerm(aWebConsoleFrame)
JSTerm.prototype = {
SELECTED_FRAME: -1,
/**
* Load the console history from previous sessions.
* @private
*/
_loadHistory: function() {
this.history = [];
this.historyIndex = this.historyPlaceHolder = 0;
this.historyLoaded = asyncStorage.getItem("webConsoleHistory").then(value => {
if (Array.isArray(value)) {
// Since it was gotten asynchronously, there could be items already in
// the history. It's not likely but stick them onto the end anyway.
this.history = value.concat(this.history);
// Holds the number of entries in history. This value is incremented in
// this.execute().
this.historyIndex = this.history.length;
// Holds the index of the history entry that the user is currently viewing.
// This is reset to this.history.length when this.execute() is invoked.
this.historyPlaceHolder = this.history.length;
}
}, console.error);
},
/**
* Clear the console history altogether. Note that this will not affect
* other consoles that are already opened (since they have their own copy),
* but it will reset the array for all newly-opened consoles.
* @returns Promise
* Resolves once the changes have been persisted.
*/
clearHistory: function() {
this.history = [];
this.historyIndex = this.historyPlaceHolder = 0;
return this.storeHistory();
},
/**
* Stores the console history for future console instances.
* @returns Promise
* Resolves once the changes have been persisted.
*/
storeHistory: function() {
return asyncStorage.setItem("webConsoleHistory", this.history);
},
/**
* Stores the data for the last completion.
* @type object
@@ -3273,6 +3286,9 @@ JSTerm.prototype = {
case "clearOutput":
this.clearOutput();
break;
case "clearHistory":
this.clearHistory();
break;
case "inspectObject":
if (aAfterMessage) {
if (!aAfterMessage._objectActors) {
@@ -3392,6 +3408,12 @@ JSTerm.prototype = {
// value that was not evaluated yet.
this.history[this.historyIndex++] = aExecuteString;
this.historyPlaceHolder = this.history.length;
if (this.history.length > this.inputHistoryCount) {
this.history.splice(0, this.history.length - this.inputHistoryCount);
this.historyIndex = this.historyPlaceHolder = this.history.length;
}
this.storeHistory();
WebConsoleUtils.usageCount++;
this.setInputValue("");
this.clearCompletion();
@@ -3883,7 +3905,7 @@ JSTerm.prototype = {
hud.groupDepth = 0;
hud._outputQueue.forEach(hud._destroyItem, hud);
hud._outputQueue = [];
hud._networkRequests = {};
this.webConsoleClient.clearNetworkRequests();
hud._repeatNodes = {};
if (aClearStorage) {
@@ -5023,8 +5045,6 @@ WebConsoleConnectionProxy.prototype = {
client.addListener("logMessage", this._onLogMessage);
client.addListener("pageError", this._onPageError);
client.addListener("consoleAPICall", this._onConsoleAPICall);
client.addListener("networkEvent", this._onNetworkEvent);
client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
client.addListener("fileActivity", this._onFileActivity);
client.addListener("reflowActivity", this._onReflowActivity);
client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
@@ -5089,6 +5109,8 @@ WebConsoleConnectionProxy.prototype = {
this.webConsoleClient = aWebConsoleClient;
this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI;
this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
let msgs = ["PageError", "ConsoleAPI"];
this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
@@ -5118,7 +5140,10 @@ WebConsoleConnectionProxy.prototype = {
Cu.reportError("Web Console getCachedMessages error: invalid state.");
}
this.owner.displayCachedMessages(aResponse.messages);
let messages = aResponse.messages.concat(...this.webConsoleClient.getNetworkEvents());
messages.sort((a, b) => a.timeStamp - b.timeStamp);
this.owner.displayCachedMessages(messages);
if (!this._hasNativeConsoleAPI) {
this.owner.logWarningAboutReplacedAPI();
@@ -5184,15 +5209,15 @@ WebConsoleConnectionProxy.prototype = {
* the UI for displaying.
*
* @private
* @param string aType
* @param string type
* Message type.
* @param object aPacket
* The message received from the server.
* @param object networkInfo
* The network request information.
*/
_onNetworkEvent: function WCCP__onNetworkEvent(aType, aPacket)
_onNetworkEvent: function(type, networkInfo)
{
if (this.owner && aPacket.from == this._consoleActor) {
this.owner.handleNetworkEvent(aPacket.eventActor);
if (this.owner) {
this.owner.handleNetworkEvent(networkInfo);
}
},
@@ -5201,16 +5226,17 @@ WebConsoleConnectionProxy.prototype = {
* the UI for displaying.
*
* @private
* @param string aType
* @param string type
* Message type.
* @param object aPacket
* @param object packet
* The message received from the server.
* @param object networkInfo
* The network request information.
*/
_onNetworkEventUpdate: function WCCP__onNetworkEvenUpdatet(aType, aPacket)
_onNetworkEventUpdate: function(type, { packet, networkInfo })
{
if (this.owner) {
this.owner.handleNetworkEventUpdate(aPacket.from, aPacket.updateType,
aPacket);
this.owner.handleNetworkEventUpdate(networkInfo, packet);
}
},
@@ -5310,11 +5336,11 @@ WebConsoleConnectionProxy.prototype = {
this.client.removeListener("logMessage", this._onLogMessage);
this.client.removeListener("pageError", this._onPageError);
this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
this.client.removeListener("networkEvent", this._onNetworkEvent);
this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
this.client.removeListener("fileActivity", this._onFileActivity);
this.client.removeListener("reflowActivity", this._onReflowActivity);
this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited);
this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
this.target.off("will-navigate", this._onTabNavigated);
this.target.off("navigate", this._onTabNavigated);
@@ -163,6 +163,13 @@ function goUpdateConsoleCommands() {
prefKey="info"/>
<menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
prefKey="log"/>
<menuseparator />
<menuitem label="&btnConsoleSharedWorkers;" type="checkbox"
autocheck="false" prefKey="sharedworkers"/>
<menuitem label="&btnConsoleServiceWorkers;" type="checkbox"
autocheck="false" prefKey="serviceworkers"/>
<menuitem label="&btnConsoleWindowlessWorkers;" type="checkbox"
autocheck="false" prefKey="windowlessworkers"/>
</menupopup>
</toolbarbutton>
</hbox>
@@ -51,14 +51,22 @@
<!-- LOCALIZATION NOTE (profilerUI.table.*): These strings are displayed
- in the call tree headers for a recording. -->
<!ENTITY profilerUI.table.totalDuration2 "Total Time">
<!ENTITY profilerUI.table.selfDuration2 "Self Time">
<!ENTITY profilerUI.table.totalPercentage "Total Cost">
<!ENTITY profilerUI.table.selfPercentage "Self Cost">
<!ENTITY profilerUI.table.samples "Samples">
<!ENTITY profilerUI.table.totalAlloc "Total Allocations">
<!ENTITY profilerUI.table.selfAlloc "Self Allocations">
<!ENTITY profilerUI.table.function "Function">
<!ENTITY profilerUI.table.totalDuration2 "Total Time">
<!ENTITY profilerUI.table.totalDuration.tooltip "The amount of time spent in this function and functions it calls.">
<!ENTITY profilerUI.table.selfDuration2 "Self Time">
<!ENTITY profilerUI.table.selfDuration.tooltip "The amount of time spent only within this function.">
<!ENTITY profilerUI.table.totalPercentage "Total Cost">
<!ENTITY profilerUI.table.totalPercentage.tooltip "The percentage of time spent in this function and functions it calls.">
<!ENTITY profilerUI.table.selfPercentage "Self Cost">
<!ENTITY profilerUI.table.selfPercentage.tooltip "The percentage of time spent only within this function.">
<!ENTITY profilerUI.table.samples "Samples">
<!ENTITY profilerUI.table.samples.tooltip "The number of times this function was on the stack when the profiler took a sample.">
<!ENTITY profilerUI.table.function "Function">
<!ENTITY profilerUI.table.function.tooltip "The name and source location of the sampled function.">
<!ENTITY profilerUI.table.totalAlloc1 "Total Sampled Allocations">
<!ENTITY profilerUI.table.totalAlloc.tooltip "The total number of Object allocations sampled at this location and in callees.">
<!ENTITY profilerUI.table.selfAlloc1 "Self Sampled Allocations">
<!ENTITY profilerUI.table.selfAlloc.tooltip "The number of Object allocations sampled at this location.">
<!-- LOCALIZATION NOTE (profilerUI.newtab.tooltiptext): The tooltiptext shown
- on the "+" (new tab) button for a profile when a selection is available. -->
@@ -76,6 +76,16 @@
<!ENTITY btnConsoleXhr "XHR">
<!ENTITY btnConsoleReflows "Reflows">
<!-- LOCALIZATION NODE (btnConsoleSharedWorkers) the term "Shared Workers"
- should not be translated. -->
<!ENTITY btnConsoleSharedWorkers "Shared Workers">
<!-- LOCALIZATION NODE (btnConsoleServiceWorkers) the term "Service Workers"
- should not be translated. -->
<!ENTITY btnConsoleServiceWorkers "Service Workers">
<!-- LOCALIZATION NODE (btnConsoleWindowlessWorkers) the term "Workers"
- should not be translated. -->
<!ENTITY btnConsoleWindowlessWorkers "Add-on or Chrome Workers">
<!ENTITY filterOutput.placeholder "Filter output">
<!ENTITY btnClear.label "Clear">
<!ENTITY btnClear.tooltip "Clear the Web Console output">
@@ -99,7 +99,7 @@ body {
background-image: url("debugger-play.png");
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#element-picker::before {
background-image: url("chrome://global/skin/devtools/command-pick@2x.png");
background-size: 64px;
@@ -351,7 +351,7 @@ body {
background-image: url(rewind.png);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.timeline .toggle::before {
background-image: url(debugger-pause@2x.png);
}
@@ -145,7 +145,7 @@
list-style-image: url(debugger-step-out.png);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#resume {
list-style-image: url(debugger-play@2x.png);
-moz-image-region: rect(0px,64px,32px,32px);
@@ -250,7 +250,7 @@
background-size: 12px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.selected .call-item-gutter {
background-image: url("editor-debug-location@2x.png");
}
@@ -82,7 +82,7 @@
-moz-image-region: rect(0px, 64px, 16px, 48px);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#developer-toolbar-toolbox-button {
list-style-image: url("chrome://global/skin/devtools/toggle-tools@2x.png");
-moz-image-region: rect(0px, 32px, 32px, 0px);
@@ -111,7 +111,7 @@
opacity: 0.6;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#developer-toolbar-closebutton {
list-style-image: url("chrome://global/skin/devtools/close@2x.png");
}
@@ -199,7 +199,7 @@ html|*#gcli-output-frame {
background-position: -16px center;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.gclitoolbar-input-node::before {
background-image: url("chrome://global/skin/devtools/commandline-icon@2x.png");
}
@@ -61,7 +61,7 @@ body {
background-size: 5px 8px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.property-value, .other-property-value {
background-image: url(arrow-e@2x.png);
}
@@ -334,7 +334,7 @@ div.CodeMirror span.eval-text {
background-position: -42px 0;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.theme-twisty, .theme-checkbox {
background-image: url("chrome://global/skin/devtools/controls@2x.png");
}
@@ -372,7 +372,7 @@ div.CodeMirror span.eval-text {
margin-left: -4px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.theme-tooltip-panel .panel-arrow[side="top"],
.theme-tooltip-panel .panel-arrow[side="bottom"] {
list-style-image: url("chrome://global/skin/devtools/tooltip/arrow-vertical-dark@2x.png");
+10 -10
View File
@@ -66,7 +66,7 @@
list-style-image: url(debugger-blackbox.png);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#black-box {
list-style-image: url(debugger-blackbox@2x.png);
}
@@ -76,7 +76,7 @@
list-style-image: url(debugger-prettyprint.png);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#pretty-print {
list-style-image: url(debugger-prettyprint@2x.png);
}
@@ -86,7 +86,7 @@
list-style-image: url(debugger-toggleBreakpoints.png);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#toggle-breakpoints {
list-style-image: url(debugger-toggleBreakpoints@2x.png);
}
@@ -100,7 +100,7 @@
-moz-image-region: rect(0px,32px,16px,16px);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#sources-toolbar .devtools-toolbarbutton:not([label]) {
-moz-image-region: rect(0px,32px,32px,0px);
}
@@ -134,7 +134,7 @@
-moz-margin-end: 5px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#black-boxed-message-button > .button-box > .button-icon {
background-image: url(debugger-blackbox@2x.png);
}
@@ -222,7 +222,7 @@
-moz-image-region: rect(0px,32px,16px,16px);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#trace {
list-style-image: url(tracer-icon@2x.png);
-moz-image-region: rect(0px,32px,32px,0px);
@@ -324,7 +324,7 @@
margin: 2px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.dbg-expression-arrow {
background-image: url(commandline-icon@2x.png);
}
@@ -560,7 +560,7 @@
list-style-image: url(debugger-play.png);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#resume {
list-style-image: url(debugger-pause@2x.png);
-moz-image-region: rect(0px,32px,32px,0px);
@@ -592,7 +592,7 @@
list-style-image: url(debugger-step-out.png);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#step-over {
list-style-image: url(debugger-step-over@2x.png);
}
@@ -622,7 +622,7 @@
-moz-image-region: rect(0px,32px,16px,16px);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#instruments-pane-toggle {
list-style-image: url(debugger-collapse@2x.png);
-moz-image-region: rect(0px,32px,32px,0px);
+1 -1
View File
@@ -62,7 +62,7 @@
-moz-image-region: rect(0px,32px,16px,16px);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#inspector-pane-toggle {
list-style-image: url(debugger-collapse@2x.png);
-moz-image-region: rect(0px,32px,32px,0px);
@@ -343,7 +343,7 @@ div.CodeMirror span.eval-text {
background-position: -14px 0;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.theme-twisty, .theme-checkbox {
background-image: url("chrome://global/skin/devtools/controls@2x.png");
}
@@ -381,7 +381,7 @@ div.CodeMirror span.eval-text {
margin-left: -4px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.theme-tooltip-panel .panel-arrow[side="top"],
.theme-tooltip-panel .panel-arrow[side="bottom"] {
list-style-image: url("chrome://global/skin/devtools/tooltip/arrow-vertical-light@2x.png");
@@ -477,7 +477,7 @@ label.requests-menu-status-code {
-moz-image-region: rect(0px,32px,16px,16px);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#details-pane-toggle {
list-style-image: url("chrome://global/skin/devtools/debugger-collapse@2x.png");
-moz-image-region: rect(0px,32px,32px,0px);
@@ -606,7 +606,7 @@ label.requests-menu-status-code {
height: 12px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.security-warning-icon {
background-image: url(alerticon-warning@2x.png);
}
+268 -276
View File
@@ -6,6 +6,7 @@
/* CSS Variables specific to this panel that aren't defined by the themes */
.theme-dark {
--cell-border-color: rgba(255,255,255,0.15);
--cell-border-color-light: rgba(255,255,255,0.1);
--focus-cell-border-color: rgba(255,255,255,0.5);
--row-alt-background-color: rgba(29,79,115,0.15);
--row-hover-background-color: rgba(29,79,115,0.25);
@@ -13,6 +14,7 @@
.theme-light {
--cell-border-color: rgba(0,0,0,0.15);
--cell-border-color-light: rgba(0,0,0,0.1);
--focus-cell-border-color: rgba(0,0,0,0.3);
--row-alt-background-color: rgba(76,158,217,0.1);
--row-hover-background-color: rgba(76,158,217,0.2);
@@ -20,19 +22,10 @@
/* Toolbar */
#performance-toolbar > tabs,
#performance-toolbar {
-moz-border-end-color: var(--theme-splitter-color);
}
#performance-toolbar-control-other {
-moz-padding-end: 5px;
}
#performance-toolbar-controls-detail-views > toolbarbutton {
min-width: 0;
}
#performance-toolbar-controls-detail-views .toolbarbutton-text {
-moz-padding-start: 4px;
-moz-padding-end: 8px;
@@ -60,117 +53,7 @@
border-radius: 1px;
}
/* Recording Notice */
#performance-view .notice-container {
font-size: 120%;
background-color: var(--theme-toolbar-background);
color: var(--theme-body-color);
padding-bottom: 20vh;
}
#performance-view .notice-container button {
min-width: 30px;
min-height: 28px;
margin: 0;
}
#performance-view .notice-container vbox {
text-align: center;
}
.console-profile-command {
font-family: monospace;
margin: 3px 2px;
}
.theme-dark #performance-view toolbarbutton.record-button {
background-color: var(--theme-tab-toolbar-background);
border: 1px solid var(--theme-sidebar-background);
}
.theme-light #performance-view toolbarbutton.record-button {
background-color: rgba(221, 221, 221, 1);
border: 1px solid rgba(204, 204, 204, 1);
}
#performance-view toolbarbutton.record-button {
margin: 5px;
padding: 5px;
cursor: pointer;
text-shadow: none;
}
#performance-view .realtime-message {
opacity: 0.5;
display: block;
}
#performance-view toolbarbutton.record-button[checked],
#performance-view toolbarbutton.record-button[checked] {
color: var(--theme-selection-color);
background: var(--theme-selection-background);
}
#performance-view .realtime-disabled-message,
#performance-view .realtime-disabled-on-e10s-message {
display: none;
}
#performance-view[e10s="disabled"] .realtime-disabled-on-e10s-message {
display: block;
opacity: 0.5;
}
#performance-view[e10s="unsupported"] .realtime-disabled-message {
display: block;
opacity: 0.5;
}
#details-pane-container .buffer-status-message,
#details-pane-container .buffer-status-message-full {
display: none;
}
#details-pane-container[buffer-status="in-progress"] .buffer-status-message {
display: block;
opacity: 0.5;
}
#details-pane-container[buffer-status="full"] .buffer-status-message {
display: block;
color: var(--theme-highlight-red);
font-weight: bold;
opacity: 1;
}
#details-pane-container[buffer-status="full"] .buffer-status-message-full {
display: block;
}
/* Overview Panel */
#main-record-button {
list-style-image: url(profiler-stopwatch.svg);
}
#main-record-button[checked] {
list-style-image: url(profiler-stopwatch-checked.svg);
}
#main-record-button[locked] {
pointer-events: none;
}
#main-record-button .button-icon {
margin: 0;
}
#main-record-button .button-text {
display: none;
}
/* Details Panel */
/* Details panel buttons */
#select-waterfall-view {
list-style-image: url(performance-icons.svg#details-waterfall);
@@ -186,11 +69,126 @@
list-style-image: url(performance-icons.svg#details-flamegraph);
}
/* Recording buttons */
#main-record-button {
list-style-image: url(profiler-stopwatch.svg);
}
#main-record-button[checked] {
list-style-image: url(profiler-stopwatch-checked.svg);
}
#main-record-button .button-icon {
margin: 0;
}
#main-record-button .button-text {
display: none;
}
.notice-container .record-button {
padding: 5px !important;
}
.notice-container .record-button[checked],
.notice-container .record-button[checked] {
color: var(--theme-selection-color) !important;
background: var(--theme-selection-background) !important;
}
.record-button[locked] {
pointer-events: none;
opacity: 0.5;
}
/* Sidebar & recording items */
.recording-item {
padding: 4px;
}
.recording-item-title {
font-size: 110%;
}
.recording-item-footer {
padding-top: 4px;
font-size: 90%;
}
.recording-item-save {
text-decoration: underline;
cursor: pointer;
}
.recording-item-duration,
.recording-item-save {
color: var(--theme-body-color-alt);
}
#recordings-list .selected label {
/* Text inside a selected item should not be custom colored. */
color: inherit !important;
}
/* Recording notices */
.notice-container {
font-size: 120%;
background-color: var(--theme-toolbar-background);
color: var(--theme-body-color);
padding-bottom: 20vh;
}
.console-profile-command {
font-family: monospace;
margin: 3px 2px;
}
.realtime-disabled-message,
.realtime-disabled-on-e10s-message {
display: none;
/* This label does not want to wrap naturally (based on some combination of
it's parents and flex). Quick and dirty way to force it to wrap -> don't
let it get bigger than half the screen size */
max-width: 60vw;
}
#performance-view[e10s="disabled"] .realtime-disabled-on-e10s-message {
display: auto;
opacity: 0.5;
}
#performance-view[e10s="unsupported"] .realtime-disabled-message {
display: auto;
opacity: 0.5;
}
.buffer-status-message,
.buffer-status-message-full {
display: none;
}
#details-pane-container[buffer-status="in-progress"] .buffer-status-message {
display: auto;
opacity: 0.5;
}
#details-pane-container[buffer-status="full"] .buffer-status-message {
display: auto;
color: var(--theme-highlight-red);
font-weight: bold;
opacity: 1;
}
#details-pane-container[buffer-status="full"] .buffer-status-message-full {
display: auto;
}
/* Profile call tree */
.call-tree-cells-container {
/* Hack: force hardware acceleration */
transform: translateZ(1px);
overflow: auto;
}
@@ -227,7 +225,7 @@
.call-tree-cell[type="allocations"],
.call-tree-header[type="self-allocations"],
.call-tree-cell[type="self-allocations"] {
width: 7vw;
width: 9vw;
}
.call-tree-header[type="function"],
@@ -262,7 +260,7 @@
background-color: var(--theme-tab-toolbar-background);
}
.call-tree-item:last-child:not(:focus) {
.call-tree-item:last-child {
border-bottom: 1px solid var(--cell-border-color);
}
@@ -278,7 +276,7 @@
background-color: var(--theme-selection-background);
}
.call-tree-item:focus label {
.call-tree-item:focus description {
color: var(--theme-selection-color) !important;
}
@@ -294,8 +292,11 @@
opacity: 0.6;
}
.call-tree-name {
-moz-margin-end: 4px !important;
}
.call-tree-url {
-moz-margin-start: 4px !important;
cursor: pointer;
}
@@ -322,113 +323,19 @@
color: var(--theme-content-color2);
}
.call-tree-name[value=""],
.call-tree-url[value=""],
.call-tree-line[value=""],
.call-tree-column[value=""],
.call-tree-host[value=""] {
display: none;
}
.call-tree-zoom {
-moz-appearance: none;
background-color: transparent;
background-position: center;
background-repeat: no-repeat;
background-size: 11px;
min-width: 11px;
-moz-margin-start: 8px !important;
cursor: zoom-in;
opacity: 0;
}
.theme-dark .call-tree-zoom {
background-image: url(magnifying-glass.png);
}
.theme-light .call-tree-zoom {
background-image: url(magnifying-glass-light.png);
}
@media (min-resolution: 2dppx) {
.theme-dark .call-tree-zoom {
background-image: url(magnifying-glass@2x.png);
}
.theme-light .call-tree-zoom {
background-image: url(magnifying-glass-light@2x.png);
}
}
.call-tree-item:hover .call-tree-zoom {
transition: opacity 0.3s ease-in;
opacity: 1;
}
.call-tree-item:hover .call-tree-zoom:hover {
opacity: 0;
}
.call-tree-category {
transform: scale(0.75);
transform-origin: center right;
}
/**
* Details Waterfall Styles
* Waterfall ticks header
*/
.waterfall-list-contents {
/* Hack: force hardware acceleration */
transform: translateZ(1px);
overflow-x: hidden;
overflow-y: auto;
}
.waterfall-header-contents {
overflow-x: hidden;
}
.waterfall-background-ticks {
/* Background created on a <canvas> in js. */
/* @see browser/devtools/timeline/widgets/waterfall.js */
background-image: -moz-element(#waterfall-background);
background-repeat: repeat-y;
background-position: -1px center;
}
.waterfall-marker-container[is-spacer] {
pointer-events: none;
}
.theme-dark .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
background-color: rgba(255,255,255,0.03);
}
.theme-light .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
background-color: rgba(128,128,128,0.03);
}
.theme-dark .waterfall-marker-container:hover {
background-color: rgba(255,255,255,0.1) !important;
}
.theme-light .waterfall-marker-container:hover {
background-color: rgba(128,128,128,0.1) !important;
}
.waterfall-marker-item {
.waterfall-header-ticks {
overflow: hidden;
}
.waterfall-sidebar {
-moz-border-end: 1px solid var(--theme-splitter-color);
}
.waterfall-marker-container:hover > .waterfall-sidebar {
background-color: transparent;
}
.waterfall-header-name {
padding: 2px 4px;
font-size: 90%;
@@ -445,6 +352,104 @@
-moz-margin-start: -100px !important; /* Don't affect layout. */
}
.waterfall-background-ticks {
/* Background created on a <canvas> in js. */
/* @see browser/devtools/timeline/widgets/waterfall.js */
background-image: -moz-element(#waterfall-background);
background-repeat: repeat-y;
background-position: -1px center;
}
/**
* Markers waterfall breakdown
*/
#waterfall-breakdown {
overflow-x: hidden;
overflow-y: auto;
}
.theme-light .waterfall-tree-item:not([level="0"]) {
background-image: repeating-linear-gradient(
-45deg,
transparent 0px,
transparent 2px,
rgba(0,0,0,0.025) 2px,
rgba(0,0,0,0.025) 4px
);
}
.theme-dark .waterfall-tree-item:not([level="0"]) {
background-image: repeating-linear-gradient(
-45deg,
transparent 0px,
transparent 2px,
rgba(255,255,255,0.05) 2px,
rgba(255,255,255,0.05) 4px
);
}
.theme-light .waterfall-tree-item[expandable] .waterfall-marker-bullet,
.theme-light .waterfall-tree-item[expandable] .waterfall-marker-bar {
background-image: repeating-linear-gradient(
-45deg,
transparent 0px,
transparent 5px,
rgba(255,255,255,0.35) 5px,
rgba(255,255,255,0.35) 10px
);
}
.theme-dark .waterfall-tree-item[expandable] .waterfall-marker-bullet,
.theme-dark .waterfall-tree-item[expandable] .waterfall-marker-bar {
background-image: repeating-linear-gradient(
-45deg,
transparent 0px,
transparent 5px,
rgba(0,0,0,0.35) 5px,
rgba(0,0,0,0.35) 10px
);
}
.waterfall-tree-item[expanded],
.waterfall-tree-item:not([level="0"]) + .waterfall-tree-item[level="0"] {
box-shadow: 0 -1px var(--cell-border-color-light);
}
.waterfall-tree-item:nth-child(2n) > .waterfall-marker {
background-color: var(--row-alt-background-color);
}
.waterfall-tree-item:hover {
background-color: var(--row-hover-background-color);
}
.waterfall-tree-item:last-child {
border-bottom: 1px solid var(--cell-border-color);
}
.waterfall-tree-item:focus {
background-color: var(--theme-selection-background);
}
.waterfall-tree-item:focus description {
color: var(--theme-selection-color) !important;
}
/**
* Marker left sidebar
*/
.waterfall-sidebar {
-moz-border-end: 1px solid var(--cell-border-color);
}
.waterfall-tree-item > .waterfall-sidebar:hover,
.waterfall-tree-item:hover > .waterfall-sidebar,
.waterfall-tree-item:focus > .waterfall-sidebar {
background: transparent;
}
.waterfall-marker-bullet {
width: 8px;
height: 8px;
@@ -458,23 +463,40 @@
padding-bottom: 1px !important;
}
/**
* Marker timebar
*/
.waterfall-marker {
overflow: hidden;
}
.waterfall-marker-bar {
height: 9px;
transform-origin: left center;
border-radius: 1px;
}
.waterfall-marker-container.selected > .waterfall-sidebar,
.waterfall-marker-container.selected > .waterfall-marker-item {
background-color: var(--theme-selection-background);
color: var(--theme-selection-color);
.waterfall-marker > .theme-twisty {
/* Don't affect layout. */
width: 14px;
-moz-margin-end: -14px;
}
/**
* Marker details view
*/
#waterfall-details {
-moz-padding-start: 8px;
-moz-padding-end: 8px;
padding-top: 2vh;
overflow: auto;
min-width: 50px;
}
#waterfall-details > * {
padding-top: 3px;
}
.marker-details-bullet {
@@ -483,6 +505,23 @@
border-radius: 1px;
}
.marker-details-labelname {
-moz-padding-end: 4px;
}
.marker-details-type {
font-size: 1.2em;
font-weight: bold;
}
.marker-details-duration {
font-weight: bold;
}
/**
* Marker colors
*/
menuitem.marker-color-graphs-purple:before,
.marker-color-graphs-purple {
background-color: var(--theme-graphs-purple);
@@ -508,53 +547,6 @@ menuitem.marker-color-graphs-blue:before,
background-color: var(--theme-graphs-blue);
}
#waterfall-details > * {
padding-top: 3px;
}
.marker-details-labelname {
-moz-padding-end: 4px;
}
.marker-details-type {
font-size: 1.2em;
font-weight: bold;
}
.marker-details-duration {
font-weight: bold;
}
/* Recording items */
.recording-item {
padding: 4px;
}
.recording-item-title {
font-size: 110%;
}
.recording-item-footer {
padding-top: 4px;
font-size: 90%;
}
.recording-item-save {
text-decoration: underline;
cursor: pointer;
}
.recording-item-duration,
.recording-item-save {
color: var(--theme-body-color-alt);
}
#recordings-list .selected label {
/* Text inside a selected item should not be custom colored. */
color: inherit !important;
}
/**
* JIT View
*/
@@ -665,7 +657,7 @@ menuitem.marker-color-graphs-blue:before,
background-position: -16px -16px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#jit-optimizations-view .opt-icon::before {
background-image: url(chrome://browser/skin/devtools/webconsole@2x.png);
}
@@ -674,8 +666,8 @@ menuitem.marker-color-graphs-blue:before,
/**
* Configurable Options
*
* Elements can be tagged with a class and visibility is controlled via a preference being
* applied or removed.
* Elements can be tagged with a class and visibility is controlled via a
* preference being applied or removed.
*/
/**
@@ -190,7 +190,7 @@
background-image: url(newtab.png);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.theme-dark #profile-newtab-button {
background-image: url(newtab-inverted@2x.png);
}
@@ -367,7 +367,7 @@
background-image: url(magnifying-glass-light.png);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.theme-dark .call-tree-zoom {
background-image: url(magnifying-glass@2x.png);
}
@@ -155,7 +155,7 @@
list-style-image: url("chrome://global/skin/devtools/responsiveui-rotate.png");
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.devtools-responsiveui-close {
list-style-image: url("chrome://global/skin/devtools/close@2x.png");
}
@@ -174,7 +174,7 @@
-moz-image-region: rect(0px,32px,16px,16px);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.devtools-responsiveui-touch {
list-style-image: url("chrome://global/skin/devtools/responsiveui-touch@2x.png");
-moz-image-region: rect(0px,32px,32px,0px);
@@ -189,7 +189,7 @@
list-style-image: url("chrome://global/skin/devtools/responsiveui-screenshot.png");
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.devtools-responsiveui-screenshot {
list-style-image: url("chrome://global/skin/devtools/responsiveui-screenshot@2x.png");
}
@@ -321,7 +321,7 @@
border-bottom-left-radius: 12px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.devtools-responsiveui-resizebarV {
background-image: url("chrome://global/skin/devtools/responsive-vertical-resizer@2x.png");
}
+2 -2
View File
@@ -107,7 +107,7 @@
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.ruleview-warning {
background-image: url(alerticon-warning@2x.png);
}
@@ -194,7 +194,7 @@
background-size: 1em;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.ruleview-bezierswatch {
background: url("chrome://global/skin/devtools/cubic-bezier-swatch@2x.png");
background-size: 1em;
@@ -60,7 +60,7 @@
border: 0;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.side-menu-widget-item-checkbox .checkbox-check {
background-image: url(itemToggle@2x.png);
}
@@ -115,7 +115,7 @@
height: 40px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.stylesheet-enabled {
background-image: url(itemToggle@2x.png);
}
@@ -317,7 +317,7 @@
background-repeat: no-repeat;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.devtools-button::before {
background-size: 32px;
}
@@ -438,7 +438,7 @@
-moz-image-region: rect(0, 32px, 16px, 16px);
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.theme-dark .devtools-searchinput {
background-image: url(magnifying-glass@2x.png);
}
@@ -763,10 +763,10 @@
}
#command-button-rulers > image {
background-image: url("chrome://browser/skin/devtools/command-rulers.png");
background-image: url("chrome://global/skin/devtools/command-rulers.png");
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#command-button-paintflashing > image {
background-image: url("chrome://global/skin/devtools/command-paintflashing@2x.png");
}
@@ -187,7 +187,7 @@ text {
-moz-box-flex: 1;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
#inspector-pane-toggle {
list-style-image: url(debugger-collapse@2x.png);
-moz-image-region: rect(0px,32px,32px,0px);
@@ -52,7 +52,7 @@ a {
display: inline-block;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.message > .icon::before {
background-image: url(chrome://global/skin/devtools/webconsole@2x.png);
}
@@ -361,7 +361,7 @@ a {
background-size: 16px 16px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.jsterm-input-node {
background-image: -moz-image-rect(url('chrome://global/skin/devtools/commandline-icon@2x.png'), 0, 64, 32, 32);
}
@@ -111,7 +111,7 @@
padding: 0;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.scrollbutton-up > .toolbarbutton-icon,
.scrollbutton-down > .toolbarbutton-icon {
background-image: url("breadcrumbs-scrollbutton@2x.png");
@@ -641,7 +641,7 @@
height: 16px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.variable-or-property-non-writable-icon {
background-image: url("chrome://global/skin/devtools/vview-lock@2x.png");
}
@@ -741,7 +741,7 @@
height: 16px;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.variables-view-delete {
background-image: url("chrome://global/skin/devtools/vview-delete@2x.png");
}
@@ -767,7 +767,7 @@
cursor: pointer;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.variables-view-edit {
background-image: url("chrome://global/skin/devtools/vview-edit@2x.png");
}
@@ -793,7 +793,7 @@
cursor: pointer;
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.variables-view-open-inspector {
background-image: url("chrome://global/skin/devtools/vview-open-inspector@2x.png");
}
@@ -1386,7 +1386,7 @@
}
}
@media (min-resolution: 2dppx) {
@media (min-resolution: 1.25dppx) {
.tree-widget-item:before {
background-image: url("chrome://global/skin/devtools/controls@2x.png");
}
@@ -32,7 +32,6 @@
#include "nsIDOMDataChannel.h"
#endif
#include "nsIDOMDataTransfer.h"
#include "nsIDOMDeviceStorage.h"
#include "nsIDOMDOMCursor.h"
#include "nsIDOMDOMException.h"
#include "nsIDOMDOMRequest.h"
@@ -353,7 +352,6 @@ const ComponentsInterfaceShimEntry kComponentsInterfaceShimMap[] =
#endif
DEFINE_SHIM(DataContainerEvent),
DEFINE_SHIM(DataTransfer),
DEFINE_SHIM(DeviceStorage),
DEFINE_SHIM(DOMCursor),
DEFINE_SHIM(DOMException),
DEFINE_SHIM(DOMRequest),