Bug 1762482 - update PerformanceMeasure to User Timing L3. r=sefeng,smaug

Differential Revision: https://phabricator.services.mozilla.com/D143858
This commit is contained in:
Michael Comella
2022-05-31 16:48:14 +00:00
parent a21c7fcd9f
commit e3152947e6
13 changed files with 277 additions and 167 deletions

View File

@@ -18,6 +18,7 @@
#include "PerformanceWorker.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/dom/MessagePortBinding.h"
#include "mozilla/dom/PerformanceBinding.h"
#include "mozilla/dom/PerformanceEntryEvent.h"
#include "mozilla/dom/PerformanceNavigationBinding.h"
@@ -33,6 +34,12 @@
namespace mozilla::dom {
enum class Performance::ResolveTimestampAttribute {
Start,
End,
Duration,
};
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Performance)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
@@ -387,7 +394,7 @@ void Performance::ClearMarks(const Optional<nsAString>& aName) {
ClearUserEntries(aName, u"mark"_ns);
}
DOMHighResTimeStamp Performance::ResolveTimestampFromName(
DOMHighResTimeStamp Performance::ConvertMarkToTimestampWithString(
const nsAString& aName, ErrorResult& aRv) {
AutoTArray<RefPtr<PerformanceEntry>, 1> arr;
Optional<nsAString> typeParam;
@@ -400,7 +407,9 @@ DOMHighResTimeStamp Performance::ResolveTimestampFromName(
}
if (!IsPerformanceTimingAttribute(aName)) {
aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
nsPrintfCString errorMsg("Given mark name, %s, is unknown",
NS_ConvertUTF16toUTF8(aName).get());
aRv.ThrowSyntaxError(errorMsg);
return 0;
}
@@ -413,41 +422,190 @@ DOMHighResTimeStamp Performance::ResolveTimestampFromName(
return ts - CreationTime();
}
void Performance::Measure(const nsAString& aName,
const Optional<nsAString>& aStartMark,
DOMHighResTimeStamp Performance::ConvertMarkToTimestampWithDOMHighResTimeStamp(
const ResolveTimestampAttribute aAttribute,
const DOMHighResTimeStamp aTimestamp, ErrorResult& aRv) {
if (aTimestamp < 0) {
nsAutoCString attributeName;
switch (aAttribute) {
case ResolveTimestampAttribute::Start:
attributeName = "start";
break;
case ResolveTimestampAttribute::End:
attributeName = "end";
break;
case ResolveTimestampAttribute::Duration:
attributeName = "duration";
break;
}
nsPrintfCString errorMsg("Given attribute %s cannot be negative",
attributeName.get());
aRv.ThrowTypeError(errorMsg);
}
return aTimestamp;
}
DOMHighResTimeStamp Performance::ConvertMarkToTimestamp(
const ResolveTimestampAttribute aAttribute,
const OwningStringOrDouble& aMarkNameOrTimestamp, ErrorResult& aRv) {
if (aMarkNameOrTimestamp.IsString()) {
return ConvertMarkToTimestampWithString(aMarkNameOrTimestamp.GetAsString(),
aRv);
}
return ConvertMarkToTimestampWithDOMHighResTimeStamp(
aAttribute, aMarkNameOrTimestamp.GetAsDouble(), aRv);
}
DOMHighResTimeStamp Performance::ResolveEndTimeForMeasure(
const Optional<nsAString>& aEndMark,
ErrorResult& aRv) {
// We add nothing when 'privacy.resistFingerprinting' is on.
if (nsContentUtils::ShouldResistFingerprinting()) {
return;
}
DOMHighResTimeStamp startTime;
const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv) {
DOMHighResTimeStamp endTime;
if (aStartMark.WasPassed()) {
startTime = ResolveTimestampFromName(aStartMark.Value(), aRv);
if (NS_WARN_IF(aRv.Failed())) {
return;
}
} else {
// Navigation start is used in this case, but since DOMHighResTimeStamp is
// in relation to navigation start, this will be zero if a name is not
// passed.
startTime = 0;
}
if (aEndMark.WasPassed()) {
endTime = ResolveTimestampFromName(aEndMark.Value(), aRv);
if (NS_WARN_IF(aRv.Failed())) {
return;
endTime = ConvertMarkToTimestampWithString(aEndMark.Value(), aRv);
} else if (aOptions && aOptions->mEnd.WasPassed()) {
endTime = ConvertMarkToTimestamp(ResolveTimestampAttribute::End,
aOptions->mEnd.Value(), aRv);
} else if (aOptions && aOptions->mStart.WasPassed() &&
aOptions->mDuration.WasPassed()) {
const DOMHighResTimeStamp start = ConvertMarkToTimestamp(
ResolveTimestampAttribute::Start, aOptions->mStart.Value(), aRv);
if (aRv.Failed()) {
return 0;
}
const DOMHighResTimeStamp duration =
ConvertMarkToTimestampWithDOMHighResTimeStamp(
ResolveTimestampAttribute::Duration, aOptions->mDuration.Value(),
aRv);
if (aRv.Failed()) {
return 0;
}
endTime = start + duration;
} else {
endTime = Now();
}
RefPtr<PerformanceMeasure> performanceMeasure =
new PerformanceMeasure(GetParentObject(), aName, startTime, endTime);
return endTime;
}
DOMHighResTimeStamp Performance::ResolveStartTimeForMeasure(
const Maybe<const nsAString&>& aStartMark,
const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv) {
DOMHighResTimeStamp startTime;
if (aOptions && aOptions->mStart.WasPassed()) {
startTime = ConvertMarkToTimestamp(ResolveTimestampAttribute::Start,
aOptions->mStart.Value(), aRv);
} else if (aOptions && aOptions->mDuration.WasPassed() &&
aOptions->mEnd.WasPassed()) {
const DOMHighResTimeStamp duration =
ConvertMarkToTimestampWithDOMHighResTimeStamp(
ResolveTimestampAttribute::Duration, aOptions->mDuration.Value(),
aRv);
if (aRv.Failed()) {
return 0;
}
const DOMHighResTimeStamp end = ConvertMarkToTimestamp(
ResolveTimestampAttribute::End, aOptions->mEnd.Value(), aRv);
if (aRv.Failed()) {
return 0;
}
startTime = end - duration;
} else if (aStartMark) {
startTime = ConvertMarkToTimestampWithString(*aStartMark, aRv);
} else {
startTime = 0;
}
return startTime;
}
already_AddRefed<PerformanceMeasure> Performance::Measure(
JSContext* aCx, const nsAString& aName,
const StringOrPerformanceMeasureOptions& aStartOrMeasureOptions,
const Optional<nsAString>& aEndMark, ErrorResult& aRv) {
// When resisting fingerprinting, we don't add marks to the buffer. Since
// measure relies on relationships between marks in the buffer, this method
// will throw if we look for user-entered marks so we return a dummy measure
// instead of continuing. We could instead return real values for performance
// timing attributes and dummy values for user-entered marks but this adds
// complexity that doesn't seem worth the effort because these fingerprinting
// protections may not longer be necessary (since performance.now() already
// has reduced precision).
if (nsContentUtils::ShouldResistFingerprinting()) {
return do_AddRef(new PerformanceMeasure(GetParentObject(), aName, 0, 0,
JS::NullHandleValue));
}
// Maybe is more readable than using the union type directly.
Maybe<const PerformanceMeasureOptions&> options;
if (aStartOrMeasureOptions.IsPerformanceMeasureOptions()) {
options.emplace(aStartOrMeasureOptions.GetAsPerformanceMeasureOptions());
}
const bool isOptionsNotEmpty =
options.isSome() &&
(!options->mDetail.isUndefined() || options->mStart.WasPassed() ||
options->mEnd.WasPassed() || options->mDuration.WasPassed());
if (isOptionsNotEmpty) {
if (aEndMark.WasPassed()) {
aRv.ThrowTypeError(
"Cannot provide separate endMark argument if "
"PerformanceMeasureOptions argument is given");
return nullptr;
}
if (!options->mStart.WasPassed() && !options->mEnd.WasPassed()) {
aRv.ThrowTypeError(
"PerformanceMeasureOptions must have start and/or end member");
return nullptr;
}
if (options->mStart.WasPassed() && options->mDuration.WasPassed() &&
options->mEnd.WasPassed()) {
aRv.ThrowTypeError(
"PerformanceMeasureOptions cannot have all of the following members: "
"start, duration, and end");
return nullptr;
}
}
const DOMHighResTimeStamp endTime =
ResolveEndTimeForMeasure(aEndMark, options, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Convert to Maybe for consistency with options.
Maybe<const nsAString&> startMark;
if (aStartOrMeasureOptions.IsString()) {
startMark.emplace(aStartOrMeasureOptions.GetAsString());
}
const DOMHighResTimeStamp startTime =
ResolveStartTimeForMeasure(startMark, options, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
JS::Rooted<JS::Value> detail(aCx);
if (options && !options->mDetail.isNullOrUndefined()) {
StructuredSerializeOptions serializeOptions;
JS::Rooted<JS::Value> valueToClone(aCx, options->mDetail);
nsContentUtils::StructuredClone(aCx, GetParentObject(), valueToClone,
serializeOptions, &detail, aRv);
if (aRv.Failed()) {
return nullptr;
}
} else {
detail.setNull();
}
RefPtr<PerformanceMeasure> performanceMeasure = new PerformanceMeasure(
GetParentObject(), aName, startTime, endTime, detail);
InsertUserEntry(performanceMeasure);
if (profiler_thread_is_being_profiled_for_markers()) {
@@ -456,12 +614,6 @@ void Performance::Measure(const nsAString& aName,
TimeStamp endTimeStamp =
CreationTimeStamp() + TimeDuration::FromMilliseconds(endTime);
// Convert to Maybe values so that Optional types do not need to be used in
// the profiler.
Maybe<nsString> startMark;
if (aStartMark.WasPassed()) {
startMark.emplace(aStartMark.Value());
}
Maybe<nsString> endMark;
if (aEndMark.WasPassed()) {
endMark.emplace(aEndMark.Value());
@@ -477,6 +629,8 @@ void Performance::Measure(const nsAString& aName,
UserTimingMarker{}, aName, /* aIsMeasure */ true,
startMark, endMark);
}
return performanceMeasure.forget();
}
void Performance::ClearMeasures(const Optional<nsAString>& aName) {

View File

@@ -21,9 +21,13 @@ class ErrorResult;
namespace dom {
class OwningStringOrDouble;
class StringOrPerformanceMeasureOptions;
class PerformanceEntry;
class PerformanceMark;
struct PerformanceMarkOptions;
struct PerformanceMeasureOptions;
class PerformanceMeasure;
class PerformanceNavigation;
class PerformancePaintTiming;
class PerformanceObserver;
@@ -84,7 +88,9 @@ class Performance : public DOMEventTargetHelper {
void ClearMarks(const Optional<nsAString>& aName);
void Measure(const nsAString& aName, const Optional<nsAString>& aStartMark,
already_AddRefed<PerformanceMeasure> Measure(
JSContext* aCx, const nsAString& aName,
const StringOrPerformanceMeasureOptions& aStartOrMeasureOptions,
const Optional<nsAString>& aEndMark, ErrorResult& aRv);
void ClearMeasures(const Optional<nsAString>& aName);
@@ -161,9 +167,6 @@ class Performance : public DOMEventTargetHelper {
void ClearUserEntries(const Optional<nsAString>& aEntryName,
const nsAString& aEntryType);
DOMHighResTimeStamp ResolveTimestampFromName(const nsAString& aName,
ErrorResult& aRv);
virtual void DispatchBufferFullEvent() = 0;
virtual DOMHighResTimeStamp CreationTime() const = 0;
@@ -206,6 +209,28 @@ class Performance : public DOMEventTargetHelper {
private:
MOZ_ALWAYS_INLINE bool CanAddResourceTimingEntry();
void BufferEvent();
// The attributes of a PerformanceMeasureOptions that we call
// ResolveTimestamp* on.
enum class ResolveTimestampAttribute;
DOMHighResTimeStamp ConvertMarkToTimestampWithString(const nsAString& aName,
ErrorResult& aRv);
DOMHighResTimeStamp ConvertMarkToTimestampWithDOMHighResTimeStamp(
const ResolveTimestampAttribute aAttribute, const double aTimestamp,
ErrorResult& aRv);
DOMHighResTimeStamp ConvertMarkToTimestamp(
const ResolveTimestampAttribute aAttribute,
const OwningStringOrDouble& aMarkNameOrTimestamp, ErrorResult& aRv);
DOMHighResTimeStamp ResolveEndTimeForMeasure(
const Optional<nsAString>& aEndMark,
const Maybe<const PerformanceMeasureOptions&>& aOptions,
ErrorResult& aRv);
DOMHighResTimeStamp ResolveStartTimeForMeasure(
const Maybe<const nsAString&>& aStartMark,
const Maybe<const PerformanceMeasureOptions&>& aOptions,
ErrorResult& aRv);
};
} // namespace dom

View File

@@ -100,9 +100,10 @@ JSObject* PerformanceMark::WrapObject(JSContext* aCx,
void PerformanceMark::GetDetail(JSContext* aCx,
JS::MutableHandle<JS::Value> aRetval) {
// Return a copy so that the PerformanceMark.detail reference always returns
// the same value. However, the contents of detail can be mutated. The spec
// isn't clear if this is okay but it matches Chrome's behavior.
// Return a copy so that this method always returns the value it is set to
// (i.e. it'll return the same value even if the caller assigns to it). Note
// that if detail is an object, its contents can be mutated and this is
// expected.
aRetval.set(mDetail);
}

View File

@@ -13,18 +13,49 @@ using namespace mozilla::dom;
PerformanceMeasure::PerformanceMeasure(nsISupports* aParent,
const nsAString& aName,
DOMHighResTimeStamp aStartTime,
DOMHighResTimeStamp aEndTime)
DOMHighResTimeStamp aEndTime,
const JS::Handle<JS::Value>& aDetail)
: PerformanceEntry(aParent, aName, u"measure"_ns),
mStartTime(aStartTime),
mDuration(aEndTime - aStartTime) {}
mDuration(aEndTime - aStartTime),
mDetail(aDetail) {
mozilla::HoldJSObjects(this);
}
PerformanceMeasure::~PerformanceMeasure() = default;
PerformanceMeasure::~PerformanceMeasure() { mozilla::DropJSObjects(this); }
NS_IMPL_CYCLE_COLLECTION_CLASS(PerformanceMeasure)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PerformanceMeasure,
PerformanceEntry)
tmp->mDetail.setUndefined();
mozilla::DropJSObjects(tmp);
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PerformanceMeasure,
PerformanceEntry)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PerformanceMeasure,
PerformanceEntry)
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDetail)
NS_IMPL_CYCLE_COLLECTION_TRACE_END
NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(PerformanceMeasure,
PerformanceEntry)
JSObject* PerformanceMeasure::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return PerformanceMeasure_Binding::Wrap(aCx, this, aGivenProto);
}
void PerformanceMeasure::GetDetail(JSContext* aCx,
JS::MutableHandle<JS::Value> aRetval) {
// Return a copy so that this method always returns the value it is set to
// (i.e. it'll return the same value even if the caller assigns to it). Note
// that if detail is an object, its contents can be mutated and this is
// expected.
aRetval.set(mDetail);
}
size_t PerformanceMeasure::SizeOfIncludingThis(
mozilla::MallocSizeOf aMallocSizeOf) const {
return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);

View File

@@ -14,9 +14,14 @@ namespace mozilla::dom {
// http://www.w3.org/TR/user-timing/#performancemeasure
class PerformanceMeasure final : public PerformanceEntry {
public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(PerformanceMeasure,
PerformanceEntry);
PerformanceMeasure(nsISupports* aParent, const nsAString& aName,
DOMHighResTimeStamp aStartTime,
DOMHighResTimeStamp aEndTime);
DOMHighResTimeStamp aEndTime,
const JS::Handle<JS::Value>& aDetail);
virtual JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
@@ -25,6 +30,8 @@ class PerformanceMeasure final : public PerformanceEntry {
virtual DOMHighResTimeStamp Duration() const override { return mDuration; }
void GetDetail(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval);
size_t SizeOfIncludingThis(
mozilla::MallocSizeOf aMallocSizeOf) const override;
@@ -32,6 +39,9 @@ class PerformanceMeasure final : public PerformanceEntry {
virtual ~PerformanceMeasure();
DOMHighResTimeStamp mStartTime;
DOMHighResTimeStamp mDuration;
private:
JS::Heap<JS::Value> mDetail;
};
} // namespace mozilla::dom

View File

@@ -71,6 +71,14 @@ dictionary PerformanceMarkOptions {
DOMHighResTimeStamp startTime;
};
// https://w3c.github.io/user-timing/#extensions-performance-interface
dictionary PerformanceMeasureOptions {
any detail;
(DOMString or DOMHighResTimeStamp) start;
DOMHighResTimeStamp duration;
(DOMString or DOMHighResTimeStamp) end;
};
// https://w3c.github.io/user-timing/#extensions-performance-interface
[Exposed=(Window,Worker)]
partial interface Performance {
@@ -78,7 +86,7 @@ partial interface Performance {
PerformanceMark mark(DOMString markName, optional PerformanceMarkOptions markOptions = {});
void clearMarks(optional DOMString markName);
[Throws]
void measure(DOMString measureName, optional DOMString startMark, optional DOMString endMark);
PerformanceMeasure measure(DOMString measureName, optional (DOMString or PerformanceMeasureOptions) startOrMeasureOptions = {}, optional DOMString endMark);
void clearMeasures(optional DOMString measureName);
};

View File

@@ -10,4 +10,5 @@
[Exposed=(Window,Worker)]
interface PerformanceMeasure : PerformanceEntry
{
readonly attribute any detail;
};

View File

@@ -1,33 +1,2 @@
[idlharness.https.any.serviceworker.html]
expected: TIMEOUT
[idlharness.any.html]
[PerformanceMeasure interface: attribute detail]
expected: FAIL
[PerformanceMeasure interface: measure must inherit property "detail" with the proper type]
expected: FAIL
[idlharness.any.worker.html]
[PerformanceMeasure interface: attribute detail]
expected: FAIL
[PerformanceMeasure interface: measure must inherit property "detail" with the proper type]
expected: FAIL
[idlharness.any.sharedworker.html]
[PerformanceMeasure interface: attribute detail]
expected: FAIL
[PerformanceMeasure interface: measure must inherit property "detail" with the proper type]
expected: FAIL
[idlharness.any.serviceworker.html]
[PerformanceMeasure interface: attribute detail]
expected: FAIL
[PerformanceMeasure interface: measure must inherit property "detail" with the proper type]
expected: FAIL

View File

@@ -1,20 +0,0 @@
[mark-measure-return-objects.any.html]
[L3: performance.measure(name, param1, param2) should return an entry.]
expected: FAIL
[L3: performance.measure(name, param1) should return an entry.]
expected: FAIL
[L3: performance.measure(name) should return an entry.]
expected: FAIL
[mark-measure-return-objects.any.worker.html]
[L3: performance.measure(name, param1, param2) should return an entry.]
expected: FAIL
[L3: performance.measure(name, param1) should return an entry.]
expected: FAIL
[L3: performance.measure(name) should return an entry.]
expected: FAIL

View File

@@ -1,21 +0,0 @@
[measure-l3.any.html]
[When the end mark is given and the start is unprovided, the end time of the measure entry should be the end mark's time, the start time should be 0.]
expected: FAIL
[When start and end mark are both given, the start time and end time of the measure entry should be the the marks' time, repectively]
expected: FAIL
[When the start mark is given and the end is unprovided, the start time of the measure entry should be the start mark's time, the end should be now.]
expected: FAIL
[measure-l3.any.worker.html]
[When the end mark is given and the start is unprovided, the end time of the measure entry should be the end mark's time, the start time should be 0.]
expected: FAIL
[When start and end mark are both given, the start time and end time of the measure entry should be the the marks' time, repectively]
expected: FAIL
[When the start mark is given and the end is unprovided, the start time of the measure entry should be the start mark's time, the end should be now.]
expected: FAIL

View File

@@ -1,15 +0,0 @@
[measure-with-dict.any.html]
[measure entries' detail and start/end are customizable]
expected: FAIL
[measure should throw a TypeError when passed an invalid argument combination]
expected: FAIL
[measure-with-dict.any.worker.html]
[measure entries' detail and start/end are customizable]
expected: FAIL
[measure should throw a TypeError when passed an invalid argument combination]
expected: FAIL

View File

@@ -1,7 +0,0 @@
[measure_exception.html]
[Invocation of performance.measure("Exception9", {"start": 1, "duration": 2, "end": 3}) should throw TypeError Exception.]
expected: FAIL
[Invocation of performance.measure("Exception8", {"detail": "non-empty"}) should throw TypeError Exception.]
expected: FAIL

View File

@@ -1,26 +0,0 @@
[structured-serialize-detail.any.html]
[When accessing detail from a measure entry and the detail is not provided, just return a null value.]
expected: FAIL
[The detail property in the measure method should be the same reference.]
expected: FAIL
[The detail property in the measure method should be structured-clone.]
expected: FAIL
[Measure: Throw an exception when the detail property cannot be structured-serialized.]
expected: FAIL
[structured-serialize-detail.any.worker.html]
[When accessing detail from a measure entry and the detail is not provided, just return a null value.]
expected: FAIL
[The detail property in the measure method should be the same reference.]
expected: FAIL
[The detail property in the measure method should be structured-clone.]
expected: FAIL
[Measure: Throw an exception when the detail property cannot be structured-serialized.]
expected: FAIL